@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,1737 @@
1
+ import type {
2
+ ExecutionPlanToolResultData,
3
+ PlanFailureClass,
4
+ PlanNodeResultSubmission,
5
+ SerializableExecutionPlan,
6
+ } from '@lota-sdk/shared'
7
+ import { PlanNodeAttemptSchema, PlanNodeRunSchema } from '@lota-sdk/shared'
8
+ import { Context, Schema, Effect, Layer } from 'effect'
9
+ import type { z } from 'zod'
10
+
11
+ import { aiLogger } from '../../config/logger'
12
+ import type { RecordIdInput } from '../../db/record-id'
13
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
14
+ import type { DatabaseTransaction } from '../../db/service'
15
+ import { TABLES } from '../../db/tables'
16
+ import { BadRequestError, NotFoundError } from '../../effect/errors'
17
+ import { effectTryPromise as effectTryPromiseShared } from '../../effect/helpers'
18
+ import { runPromise } from '../../effect/runtime'
19
+ import { DatabaseServiceTag } from '../../effect/services'
20
+ import { GeneratedDocumentStorageServiceTag } from '../../storage/generated-document-storage.service'
21
+ import { nowDate } from '../../utils/date-time'
22
+ import { toError } from '../../utils/errors'
23
+ import { ArtifactServiceTag } from '../artifact.service'
24
+ import { BackgroundWorkService } from '../background-work.service'
25
+ import { FeedbackLoopServiceTag } from '../feedback-loop.service'
26
+ import { InstitutionalMemoryServiceTag } from '../institutional-memory.service'
27
+ import { QualityMetricsServiceTag } from '../quality-metrics.service'
28
+ import { PlanApprovalServiceTag } from './plan-approval.service'
29
+ import { PlanArtifactServiceTag } from './plan-artifact.service'
30
+ import { PlanCheckpointServiceTag } from './plan-checkpoint.service'
31
+ import { makePlanCompletionSideEffects } from './plan-completion-side-effects'
32
+ import { PlanCoordinationServiceTag } from './plan-coordination.service'
33
+ import { PlanEventDeliveryServiceTag } from './plan-event-delivery.service'
34
+ import type { PlanExecutorContext } from './plan-executor-context'
35
+ import { syncRunGraph } from './plan-executor-graph'
36
+ import type { HumanNodeResponsePayload } from './plan-executor-helpers'
37
+ import {
38
+ buildPublishedArtifactContent,
39
+ buildResolvedInput,
40
+ deriveApprovalStatus,
41
+ isHumanNodeType,
42
+ readHumanRequiredEdits,
43
+ readHumanResponseComments,
44
+ isStructuralNodeType,
45
+ resolveFailureAction,
46
+ toNodeRunData,
47
+ } from './plan-executor-helpers'
48
+ import {
49
+ attachCheckpoint,
50
+ createAttempt,
51
+ emitEvent,
52
+ persistValidationIssues,
53
+ replaceRun,
54
+ saveCheckpoint,
55
+ } from './plan-executor-persistence'
56
+ import { toPlanNodeValidationSpec } from './plan-node-spec'
57
+ import { buildExecutionPlanToolResult } from './plan-run-data'
58
+ import { serializeRunFull } from './plan-run-serialization'
59
+ import { PlanRunServiceTag } from './plan-run.service'
60
+ import { PlanSchedulerServiceTag } from './plan-scheduler.service'
61
+ import { withTransactionAndEventsEffect } from './plan-transaction-events'
62
+ import { PlanValidatorServiceTag } from './plan-validator.service'
63
+
64
+ interface PlanExecutorDeps {
65
+ db: Context.Service.Shape<typeof DatabaseServiceTag>
66
+ storage: Context.Service.Shape<typeof GeneratedDocumentStorageServiceTag>
67
+ artifactService: Context.Service.Shape<typeof ArtifactServiceTag>
68
+ feedbackLoopService: Context.Service.Shape<typeof FeedbackLoopServiceTag>
69
+ institutionalMemoryService: Context.Service.Shape<typeof InstitutionalMemoryServiceTag>
70
+ planApprovalService: Context.Service.Shape<typeof PlanApprovalServiceTag>
71
+ planArtifactService: Context.Service.Shape<typeof PlanArtifactServiceTag>
72
+ planCheckpointService: Context.Service.Shape<typeof PlanCheckpointServiceTag>
73
+ planCoordinationService: Context.Service.Shape<typeof PlanCoordinationServiceTag>
74
+ planEventDeliveryService: Context.Service.Shape<typeof PlanEventDeliveryServiceTag>
75
+ planRunService: Context.Service.Shape<typeof PlanRunServiceTag>
76
+ planSchedulerService: Context.Service.Shape<typeof PlanSchedulerServiceTag>
77
+ planValidatorService: Context.Service.Shape<typeof PlanValidatorServiceTag>
78
+ qualityMetricsService: Context.Service.Shape<typeof QualityMetricsServiceTag>
79
+ }
80
+
81
+ type PlanExecutorService = ReturnType<typeof makePlanExecutorService>
82
+
83
+ function saveCheckpointWithContext(
84
+ context: Pick<PlanExecutorContext, 'planCheckpointService'>,
85
+ params: Omit<Parameters<typeof saveCheckpoint>[0], 'planCheckpointService'>,
86
+ ) {
87
+ return saveCheckpoint({ ...params, planCheckpointService: context.planCheckpointService })
88
+ }
89
+
90
+ class PlanExecutorInternalError extends Schema.TaggedErrorClass<PlanExecutorInternalError>()(
91
+ 'PlanExecutorInternalError',
92
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
93
+ ) {}
94
+
95
+ function fromPromise<A>(thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>) {
96
+ return effectTryPromiseShared(
97
+ thunk,
98
+ (cause) => new PlanExecutorInternalError({ message: toError(cause).message, cause }),
99
+ )
100
+ }
101
+
102
+ function parseRowOrFail<T>(
103
+ schema: z.ZodType<T>,
104
+ value: unknown,
105
+ operation: string,
106
+ ): Effect.Effect<T, PlanExecutorInternalError> {
107
+ return Effect.try({
108
+ try: () => schema.parse(value),
109
+ catch: (cause) => new PlanExecutorInternalError({ message: `Failed to parse ${operation} row`, cause }),
110
+ })
111
+ }
112
+
113
+ function withDatabaseTransactionEffect<A, E, R>(
114
+ databaseService: Context.Service.Shape<typeof DatabaseServiceTag>,
115
+ run: (tx: DatabaseTransaction) => Effect.Effect<A, E, R>,
116
+ ) {
117
+ return databaseService.withTransaction((tx) => run(tx))
118
+ }
119
+
120
+ type SaveCheckpointParams = Parameters<typeof saveCheckpointWithContext>[1]
121
+ type CapturedPlanEvents = Parameters<typeof emitEvent>[0]['capturedEvents']
122
+ type EffectSuccess<T> = T extends Effect.Effect<infer A, infer _E, infer _R> ? A : Awaited<T>
123
+ type ActivePlanRunRecord = NonNullable<
124
+ EffectSuccess<ReturnType<PlanExecutorContext['planRunService']['getActiveRunRecord']>>
125
+ >
126
+ type PlanRunRecord = EffectSuccess<ReturnType<PlanExecutorContext['planRunService']['getRunById']>>
127
+ type PlanSpecRecord = EffectSuccess<ReturnType<PlanExecutorContext['planRunService']['getPlanSpecById']>>
128
+ type PlanNodeSpecRecord = EffectSuccess<ReturnType<PlanExecutorContext['planRunService']['listNodeSpecs']>>[number]
129
+ type PlanNodeRunRecord = EffectSuccess<ReturnType<PlanExecutorContext['planRunService']['getNodeRunByNodeId']>>
130
+ type PlanArtifacts = EffectSuccess<ReturnType<PlanExecutorContext['planRunService']['listArtifacts']>>
131
+ type PlanNodeResultValidation = ReturnType<PlanExecutorContext['planValidatorService']['validateNodeResult']>
132
+ type PlanApprovalRecord = NonNullable<
133
+ EffectSuccess<ReturnType<PlanExecutorContext['planApprovalService']['getApprovalById']>>
134
+ >
135
+ type ApprovalResolutionStatus = ReturnType<typeof deriveApprovalStatus>
136
+
137
+ interface LoadedNodeResultSubmission {
138
+ run: PlanRunRecord
139
+ spec: PlanSpecRecord
140
+ nodeSpecs: PlanNodeSpecRecord[]
141
+ nodeSpec: PlanNodeSpecRecord
142
+ nodeRun: PlanNodeRunRecord
143
+ existingArtifacts: PlanArtifacts
144
+ nextCheckpointSequence: number
145
+ validation: PlanNodeResultValidation
146
+ }
147
+
148
+ interface PersistedNodeResultAttempt {
149
+ attemptId: RecordIdInput
150
+ nextNodeRun: PlanNodeRunRecord
151
+ withUpdatedNodeRuns: PlanNodeRunRecord[]
152
+ nextArtifacts: PlanArtifacts
153
+ }
154
+
155
+ interface LoadedHumanNodeResponseSubmission {
156
+ run: ActivePlanRunRecord
157
+ spec: PlanSpecRecord
158
+ nodeSpecs: PlanNodeSpecRecord[]
159
+ nodeSpec: PlanNodeSpecRecord
160
+ nodeRun: PlanNodeRunRecord
161
+ approval: PlanApprovalRecord
162
+ existingArtifacts: PlanArtifacts
163
+ nextCheckpointSequence: number
164
+ validation: PlanNodeResultValidation
165
+ responseComments?: string
166
+ submittedResult: PlanNodeResultSubmission
167
+ }
168
+
169
+ interface PersistedHumanNodeResponseAttempt {
170
+ attemptId: RecordIdInput
171
+ approvalStatus: ApprovalResolutionStatus
172
+ nextNodeRun: PlanNodeRunRecord
173
+ nodeRuns: PlanNodeRunRecord[]
174
+ }
175
+
176
+ function replaceNodeRunInList<T extends { nodeId: string }>(nodeRuns: readonly T[], nextNodeRun: T): T[] {
177
+ return nodeRuns.map((candidate) => (candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate))
178
+ }
179
+
180
+ function saveAndAttachCheckpointEffect(
181
+ context: Pick<PlanExecutorContext, 'planCheckpointService'>,
182
+ params: SaveCheckpointParams,
183
+ ) {
184
+ return Effect.gen(function* () {
185
+ const checkpoint = yield* saveCheckpointWithContext(context, params)
186
+ yield* attachCheckpoint(params.tx, params.run, checkpoint)
187
+ })
188
+ }
189
+
190
+ function loadNodeResultSubmissionContextEffect(
191
+ context: Pick<PlanExecutorContext, 'planRunService' | 'planValidatorService'>,
192
+ params: { threadId: RecordIdInput; runId: string; nodeId: string; result: PlanNodeResultSubmission },
193
+ ) {
194
+ return Effect.gen(function* () {
195
+ const run = yield* context.planRunService.getRunById(params.runId)
196
+ if (recordIdToString(run.threadId, TABLES.THREAD) !== recordIdToString(params.threadId, TABLES.THREAD)) {
197
+ return yield* new BadRequestError({ message: 'Execution node result targets a different thread.' })
198
+ }
199
+ if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
200
+ return yield* new BadRequestError({ message: 'Execution run is no longer active.' })
201
+ }
202
+
203
+ const spec = yield* context.planRunService.getPlanSpecById(run.planSpecId)
204
+ const nodeSpecs = yield* context.planRunService.listNodeSpecs(spec.id)
205
+ const nodeSpec = nodeSpecs.find((candidate) => candidate.nodeId === params.nodeId)
206
+ if (!nodeSpec) {
207
+ return yield* new NotFoundError({
208
+ resource: 'plan-node',
209
+ id: params.nodeId,
210
+ message: `Execution node "${params.nodeId}" does not exist in this run.`,
211
+ })
212
+ }
213
+ if (isHumanNodeType(nodeSpec.type) || isStructuralNodeType(nodeSpec.type)) {
214
+ return yield* new BadRequestError({
215
+ message: `Execution node "${nodeSpec.label}" is executor-owned and cannot accept direct result submission.`,
216
+ })
217
+ }
218
+
219
+ const nodeRun = yield* context.planRunService.getNodeRunByNodeId(run.id, params.nodeId)
220
+ if (nodeRun.status !== 'running') {
221
+ return yield* new BadRequestError({ message: `Execution node "${nodeSpec.label}" is not currently running.` })
222
+ }
223
+
224
+ const existingArtifacts = yield* context.planRunService.listArtifacts(run.id)
225
+ const nextCheckpointSequence = yield* context.planRunService.getNextCheckpointSequence(run.id)
226
+ const validation = context.planValidatorService.validateNodeResult({
227
+ draft: { schemas: spec.schemaRegistry },
228
+ node: toPlanNodeValidationSpec(nodeSpec),
229
+ result: params.result,
230
+ })
231
+
232
+ return {
233
+ run,
234
+ spec,
235
+ nodeSpecs,
236
+ nodeSpec,
237
+ nodeRun,
238
+ existingArtifacts,
239
+ nextCheckpointSequence,
240
+ validation,
241
+ } satisfies LoadedNodeResultSubmission
242
+ })
243
+ }
244
+
245
+ function publishNodeResultArtifactsEffect(params: {
246
+ artifactService: PlanExecutorContext['artifactService']
247
+ tx: DatabaseTransaction
248
+ run: PlanRunRecord
249
+ nodeSpec: PlanNodeSpecRecord
250
+ nodeId: string
251
+ emittedBy: string
252
+ result: PlanNodeResultSubmission
253
+ publishedArtifactStorageKeys: string[]
254
+ }) {
255
+ const { artifactService, tx, run, nodeSpec, nodeId, emittedBy, result, publishedArtifactStorageKeys } = params
256
+
257
+ return Effect.forEach(result.artifacts, (artifact) =>
258
+ Effect.gen(function* () {
259
+ const deliverable = nodeSpec.deliverables.find((candidate) => candidate.name === artifact.name)
260
+ if (!deliverable?.publishAs) {
261
+ return artifact
262
+ }
263
+ const publishAs = deliverable.publishAs
264
+
265
+ const content = buildPublishedArtifactContent({ artifact, notes: result.notes })
266
+ const published = yield* artifactService
267
+ .publishArtifactInTransaction(
268
+ {
269
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
270
+ authorAgentId: emittedBy,
271
+ title: artifact.name,
272
+ artifactKind: publishAs.artifactKind,
273
+ templateId: publishAs.templateId,
274
+ canonicalKey: publishAs.canonicalKey,
275
+ content,
276
+ tags: [],
277
+ ...(artifact.description ? { description: artifact.description } : {}),
278
+ sourceThreadId: recordIdToString(run.threadId, TABLES.THREAD),
279
+ sourcePlanRunId: recordIdToString(run.id, TABLES.PLAN_RUN),
280
+ sourcePlanNodeId: nodeId,
281
+ deliverableName: artifact.name,
282
+ },
283
+ tx,
284
+ )
285
+ .pipe(Effect.mapError((cause) => new PlanExecutorInternalError({ message: toError(cause).message, cause })))
286
+ publishedArtifactStorageKeys.push(published.storageKey)
287
+
288
+ return { ...artifact, content, publishedArtifactId: recordIdToString(published.id, TABLES.ARTIFACT) }
289
+ }),
290
+ )
291
+ }
292
+
293
+ function persistNodeResultAttemptEffect(
294
+ context: Pick<PlanExecutorContext, 'artifactService' | 'planArtifactService' | 'planRunService'>,
295
+ params: {
296
+ tx: DatabaseTransaction
297
+ submission: LoadedNodeResultSubmission
298
+ nodeId: string
299
+ emittedBy: string
300
+ result: PlanNodeResultSubmission
301
+ publishedArtifactStorageKeys: string[]
302
+ },
303
+ ) {
304
+ const { tx, submission, nodeId, emittedBy, result, publishedArtifactStorageKeys } = params
305
+
306
+ return Effect.gen(function* () {
307
+ const attempt = yield* createAttempt({
308
+ tx,
309
+ run: submission.run,
310
+ nodeRun: submission.nodeRun,
311
+ emittedBy,
312
+ result,
313
+ status: submission.validation.blocking.length > 0 ? 'failed' : 'completed',
314
+ failureClass: submission.validation.failureClass,
315
+ })
316
+
317
+ const issues = yield* persistValidationIssues({
318
+ tx,
319
+ run: submission.run,
320
+ spec: submission.spec,
321
+ attemptId: attempt.id,
322
+ nodeId,
323
+ issues: [...submission.validation.blocking, ...submission.validation.warnings],
324
+ })
325
+
326
+ const updatedAttemptRow = yield* fromPromise(() =>
327
+ tx
328
+ .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
329
+ .content({
330
+ runId: ensureRecordId(attempt.runId, TABLES.PLAN_RUN),
331
+ nodeRunId: ensureRecordId(attempt.nodeRunId, TABLES.PLAN_NODE_RUN),
332
+ nodeId: attempt.nodeId,
333
+ emittedBy: attempt.emittedBy,
334
+ status: attempt.status,
335
+ ...(attempt.structuredOutput ? { structuredOutput: attempt.structuredOutput } : {}),
336
+ ...(attempt.notes ? { notes: attempt.notes } : {}),
337
+ validationIssueIds: issues.map((issue: { id: RecordIdInput }) =>
338
+ ensureRecordId(issue.id, TABLES.PLAN_VALIDATION_ISSUE),
339
+ ),
340
+ ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
341
+ })
342
+ .output('after'),
343
+ )
344
+ void (yield* parseRowOrFail(PlanNodeAttemptSchema, updatedAttemptRow, 'plan node attempt'))
345
+
346
+ const publishedArtifacts =
347
+ submission.validation.blocking.length > 0
348
+ ? result.artifacts
349
+ : yield* publishNodeResultArtifactsEffect({
350
+ artifactService: context.artifactService,
351
+ tx,
352
+ run: submission.run,
353
+ nodeSpec: submission.nodeSpec,
354
+ nodeId,
355
+ emittedBy,
356
+ result,
357
+ publishedArtifactStorageKeys,
358
+ })
359
+
360
+ const persistedArtifacts = yield* context.planArtifactService.persistArtifacts({
361
+ tx,
362
+ runId: submission.run.id,
363
+ attemptId: attempt.id,
364
+ nodeId,
365
+ artifacts: publishedArtifacts,
366
+ })
367
+
368
+ const nextNodeRunRow = yield* fromPromise(() =>
369
+ tx
370
+ .update(ensureRecordId(submission.nodeRun.id, TABLES.PLAN_NODE_RUN))
371
+ .content(
372
+ toNodeRunData(submission.nodeRun, {
373
+ attemptCount: submission.nodeRun.attemptCount + 1,
374
+ latestAttemptId: attempt.id,
375
+ latestStructuredOutput: result.structuredOutput ?? null,
376
+ latestNotes: result.notes,
377
+ }),
378
+ )
379
+ .output('after'),
380
+ )
381
+ const nextNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, nextNodeRunRow, 'plan node run')
382
+
383
+ const nodeRuns = yield* context.planRunService.listNodeRuns(submission.run.id)
384
+
385
+ return {
386
+ attemptId: attempt.id,
387
+ nextNodeRun,
388
+ withUpdatedNodeRuns: replaceNodeRunInList(nodeRuns, nextNodeRun),
389
+ nextArtifacts: [...submission.existingArtifacts, ...persistedArtifacts],
390
+ } satisfies PersistedNodeResultAttempt
391
+ })
392
+ }
393
+
394
+ function handleRetryNodeResultEffect(
395
+ context: PlanExecutorContext,
396
+ params: {
397
+ tx: DatabaseTransaction
398
+ emittedEvents: CapturedPlanEvents
399
+ submission: LoadedNodeResultSubmission
400
+ persisted: PersistedNodeResultAttempt
401
+ emittedBy: string
402
+ },
403
+ ) {
404
+ const { tx, emittedEvents, submission, persisted, emittedBy } = params
405
+
406
+ return Effect.gen(function* () {
407
+ const retryNodeRunRow = yield* fromPromise(() =>
408
+ tx
409
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
410
+ .content(
411
+ toNodeRunData(persisted.nextNodeRun, {
412
+ status: 'ready',
413
+ retryCount: persisted.nextNodeRun.retryCount + 1,
414
+ failureClass: submission.validation.failureClass,
415
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
416
+ readyAt: nowDate(),
417
+ startedAt: null,
418
+ completedAt: null,
419
+ }),
420
+ )
421
+ .output('after'),
422
+ )
423
+ const retryNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, retryNodeRunRow, 'plan node run')
424
+
425
+ yield* emitEvent({
426
+ tx,
427
+ run: submission.run,
428
+ spec: submission.spec,
429
+ nodeId: retryNodeRun.nodeId,
430
+ attemptId: persisted.attemptId,
431
+ eventType: 'validation-reported',
432
+ message: `Validation failed for node "${submission.nodeSpec.label}", scheduling retry.`,
433
+ detail: { issues: submission.validation.blocking.map((issue) => issue.code) },
434
+ emittedBy,
435
+ capturedEvents: emittedEvents,
436
+ })
437
+
438
+ yield* emitEvent({
439
+ tx,
440
+ run: submission.run,
441
+ spec: submission.spec,
442
+ nodeId: retryNodeRun.nodeId,
443
+ attemptId: persisted.attemptId,
444
+ eventType: 'node-unblocked',
445
+ fromStatus: submission.nodeRun.status,
446
+ toStatus: retryNodeRun.status,
447
+ message: `Node "${submission.nodeSpec.label}" is ready for another attempt.`,
448
+ detail: { retryCount: retryNodeRun.retryCount },
449
+ emittedBy,
450
+ capturedEvents: emittedEvents,
451
+ })
452
+
453
+ const synced = yield* fromPromise(() =>
454
+ syncRunGraph(context, {
455
+ tx,
456
+ run: submission.run,
457
+ spec: submission.spec,
458
+ nodeSpecs: submission.nodeSpecs,
459
+ nodeRuns: replaceNodeRunInList(persisted.withUpdatedNodeRuns, retryNodeRun),
460
+ artifacts: persisted.nextArtifacts,
461
+ emittedBy,
462
+ capturedEvents: emittedEvents,
463
+ }),
464
+ )
465
+
466
+ yield* saveAndAttachCheckpointEffect(context, {
467
+ tx,
468
+ run: synced.run,
469
+ spec: submission.spec,
470
+ nodeRuns: synced.nodeRuns,
471
+ artifacts: synced.artifacts,
472
+ sequence: submission.nextCheckpointSequence,
473
+ reason: 'node-result-retry',
474
+ capturedEvents: emittedEvents,
475
+ })
476
+ })
477
+ }
478
+
479
+ function handleHumanReviewNodeResultEffect(
480
+ context: PlanExecutorContext,
481
+ params: {
482
+ tx: DatabaseTransaction
483
+ emittedEvents: CapturedPlanEvents
484
+ submission: LoadedNodeResultSubmission
485
+ persisted: PersistedNodeResultAttempt
486
+ emittedBy: string
487
+ },
488
+ ) {
489
+ const { tx, emittedEvents, submission, persisted, emittedBy } = params
490
+
491
+ return Effect.gen(function* () {
492
+ const awaitingHumanNodeRunRow = yield* fromPromise(() =>
493
+ tx
494
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
495
+ .content(
496
+ toNodeRunData(persisted.nextNodeRun, {
497
+ status: 'awaiting-human',
498
+ retryCount: persisted.nextNodeRun.retryCount + 1,
499
+ failureClass: submission.validation.failureClass,
500
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
501
+ startedAt: persisted.nextNodeRun.startedAt ?? nowDate(),
502
+ }),
503
+ )
504
+ .output('after'),
505
+ )
506
+ const awaitingHumanNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, awaitingHumanNodeRunRow, 'plan node run')
507
+
508
+ const approval = yield* context.planApprovalService
509
+ .createPendingApproval({
510
+ tx,
511
+ runId: submission.run.id,
512
+ nodeRunId: awaitingHumanNodeRun.id,
513
+ nodeId: awaitingHumanNodeRun.nodeId,
514
+ requestedBy: emittedBy,
515
+ presented: {
516
+ nodeId: submission.nodeSpec.nodeId,
517
+ label: submission.nodeSpec.label,
518
+ objective: submission.nodeSpec.objective,
519
+ instructions: submission.nodeSpec.instructions,
520
+ validationIssues: submission.validation.blocking,
521
+ },
522
+ })
523
+ .pipe(Effect.mapError((cause) => new PlanExecutorInternalError({ message: toError(cause).message, cause })))
524
+
525
+ const awaitingHumanRun = yield* replaceRun(tx, submission.run, {
526
+ status: 'awaiting-human',
527
+ currentNodeId: awaitingHumanNodeRun.nodeId,
528
+ waitingNodeId: awaitingHumanNodeRun.nodeId,
529
+ readyNodeIds: [],
530
+ failureCount: submission.run.failureCount + 1,
531
+ })
532
+
533
+ yield* emitEvent({
534
+ tx,
535
+ run: awaitingHumanRun,
536
+ spec: submission.spec,
537
+ nodeId: awaitingHumanNodeRun.nodeId,
538
+ attemptId: persisted.attemptId,
539
+ approvalId: approval.id,
540
+ eventType: 'approval-requested',
541
+ fromStatus: submission.run.status,
542
+ toStatus: awaitingHumanRun.status,
543
+ message: `Node "${submission.nodeSpec.label}" requires human review before continuing.`,
544
+ detail: { issues: submission.validation.blocking.map((issue) => issue.code) },
545
+ emittedBy,
546
+ capturedEvents: emittedEvents,
547
+ })
548
+
549
+ yield* saveAndAttachCheckpointEffect(context, {
550
+ tx,
551
+ run: awaitingHumanRun,
552
+ spec: submission.spec,
553
+ nodeRuns: replaceNodeRunInList(persisted.withUpdatedNodeRuns, awaitingHumanNodeRun),
554
+ artifacts: persisted.nextArtifacts,
555
+ sequence: submission.nextCheckpointSequence,
556
+ reason: 'node-result-human-review',
557
+ capturedEvents: emittedEvents,
558
+ })
559
+ })
560
+ }
561
+
562
+ function handleReplanNodeResultEffect(
563
+ context: PlanExecutorContext,
564
+ params: {
565
+ tx: DatabaseTransaction
566
+ emittedEvents: CapturedPlanEvents
567
+ submission: LoadedNodeResultSubmission
568
+ persisted: PersistedNodeResultAttempt
569
+ emittedBy: string
570
+ },
571
+ ) {
572
+ const { tx, emittedEvents, submission, persisted, emittedBy } = params
573
+
574
+ return Effect.gen(function* () {
575
+ const blockedNodeRunRow = yield* fromPromise(() =>
576
+ tx
577
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
578
+ .content(
579
+ toNodeRunData(persisted.nextNodeRun, {
580
+ status: 'blocked',
581
+ retryCount: persisted.nextNodeRun.retryCount + 1,
582
+ failureClass: submission.validation.failureClass,
583
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
584
+ }),
585
+ )
586
+ .output('after'),
587
+ )
588
+ const blockedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, blockedNodeRunRow, 'plan node run')
589
+
590
+ const blockedRun = yield* replaceRun(tx, submission.run, {
591
+ status: 'blocked',
592
+ currentNodeId: blockedNodeRun.nodeId,
593
+ waitingNodeId: null,
594
+ readyNodeIds: [],
595
+ failureCount: submission.run.failureCount + 1,
596
+ })
597
+
598
+ yield* emitEvent({
599
+ tx,
600
+ run: blockedRun,
601
+ spec: submission.spec,
602
+ nodeId: blockedNodeRun.nodeId,
603
+ attemptId: persisted.attemptId,
604
+ eventType: 'node-blocked',
605
+ fromStatus: submission.nodeRun.status,
606
+ toStatus: blockedNodeRun.status,
607
+ message: `Node "${submission.nodeSpec.label}" failed validation and requires replanning.`,
608
+ detail: {
609
+ failureClass: submission.validation.failureClass,
610
+ issues: submission.validation.blocking.map((issue) => issue.code),
611
+ },
612
+ emittedBy,
613
+ capturedEvents: emittedEvents,
614
+ })
615
+
616
+ yield* saveAndAttachCheckpointEffect(context, {
617
+ tx,
618
+ run: blockedRun,
619
+ spec: submission.spec,
620
+ nodeRuns: replaceNodeRunInList(persisted.withUpdatedNodeRuns, blockedNodeRun),
621
+ artifacts: persisted.nextArtifacts,
622
+ sequence: submission.nextCheckpointSequence,
623
+ reason: 'node-result-replan',
624
+ capturedEvents: emittedEvents,
625
+ })
626
+ })
627
+ }
628
+
629
+ function handleFailedNodeResultEffect(
630
+ context: PlanExecutorContext,
631
+ params: {
632
+ tx: DatabaseTransaction
633
+ emittedEvents: CapturedPlanEvents
634
+ submission: LoadedNodeResultSubmission
635
+ persisted: PersistedNodeResultAttempt
636
+ emittedBy: string
637
+ },
638
+ ) {
639
+ const { tx, emittedEvents, submission, persisted, emittedBy } = params
640
+
641
+ return Effect.gen(function* () {
642
+ const failedNodeRunRow = yield* fromPromise(() =>
643
+ tx
644
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
645
+ .content(
646
+ toNodeRunData(persisted.nextNodeRun, {
647
+ status: 'failed',
648
+ retryCount: persisted.nextNodeRun.retryCount + 1,
649
+ failureClass: submission.validation.failureClass,
650
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
651
+ completedAt: nowDate(),
652
+ }),
653
+ )
654
+ .output('after'),
655
+ )
656
+ const failedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, failedNodeRunRow, 'plan node run')
657
+
658
+ const failedRun = yield* replaceRun(tx, submission.run, {
659
+ status: 'failed',
660
+ currentNodeId: null,
661
+ waitingNodeId: null,
662
+ readyNodeIds: [],
663
+ failureCount: submission.run.failureCount + 1,
664
+ completedAt: nowDate(),
665
+ })
666
+
667
+ yield* emitEvent({
668
+ tx,
669
+ run: failedRun,
670
+ spec: submission.spec,
671
+ nodeId: failedNodeRun.nodeId,
672
+ attemptId: persisted.attemptId,
673
+ eventType: 'node-failed',
674
+ fromStatus: submission.nodeRun.status,
675
+ toStatus: failedNodeRun.status,
676
+ message: `Node "${submission.nodeSpec.label}" failed validation and the run has been aborted.`,
677
+ detail: {
678
+ failureClass: submission.validation.failureClass,
679
+ issues: submission.validation.blocking.map((issue) => issue.code),
680
+ },
681
+ emittedBy,
682
+ capturedEvents: emittedEvents,
683
+ })
684
+
685
+ yield* saveAndAttachCheckpointEffect(context, {
686
+ tx,
687
+ run: failedRun,
688
+ spec: submission.spec,
689
+ nodeRuns: replaceNodeRunInList(persisted.withUpdatedNodeRuns, failedNodeRun),
690
+ artifacts: persisted.nextArtifacts,
691
+ sequence: submission.nextCheckpointSequence,
692
+ reason: 'node-result-failed',
693
+ capturedEvents: emittedEvents,
694
+ })
695
+ })
696
+ }
697
+
698
+ function handleBlockedNodeResultEffect(
699
+ context: PlanExecutorContext,
700
+ params: {
701
+ tx: DatabaseTransaction
702
+ emittedEvents: CapturedPlanEvents
703
+ submission: LoadedNodeResultSubmission
704
+ persisted: PersistedNodeResultAttempt
705
+ emittedBy: string
706
+ },
707
+ ) {
708
+ const { submission, persisted } = params
709
+
710
+ return Effect.gen(function* () {
711
+ const shouldRetry =
712
+ submission.validation.failureClass &&
713
+ submission.nodeSpec.retryPolicy.maxAttempts > persisted.nextNodeRun.retryCount &&
714
+ (submission.nodeSpec.retryPolicy.retryOn.length === 0 ||
715
+ submission.nodeSpec.retryPolicy.retryOn.includes(submission.validation.failureClass))
716
+
717
+ if (shouldRetry) {
718
+ return yield* handleRetryNodeResultEffect(context, params)
719
+ }
720
+
721
+ const failureAction = resolveFailureAction(submission.nodeSpec, submission.validation.failureClass)
722
+ if (failureAction === 'human-review') {
723
+ return yield* handleHumanReviewNodeResultEffect(context, params)
724
+ }
725
+ if (failureAction === 'replan') {
726
+ return yield* handleReplanNodeResultEffect(context, params)
727
+ }
728
+
729
+ return yield* handleFailedNodeResultEffect(context, params)
730
+ })
731
+ }
732
+
733
+ function handleSuccessfulNodeResultEffect(
734
+ context: PlanExecutorContext,
735
+ params: {
736
+ tx: DatabaseTransaction
737
+ emittedEvents: CapturedPlanEvents
738
+ submission: LoadedNodeResultSubmission
739
+ persisted: PersistedNodeResultAttempt
740
+ emittedBy: string
741
+ result: PlanNodeResultSubmission
742
+ },
743
+ ) {
744
+ const { tx, emittedEvents, submission, persisted, emittedBy, result } = params
745
+
746
+ return Effect.gen(function* () {
747
+ const completedNodeRunRow = yield* fromPromise(() =>
748
+ tx
749
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
750
+ .content(
751
+ toNodeRunData(persisted.nextNodeRun, {
752
+ status: submission.validation.warnings.length > 0 ? 'partial' : 'completed',
753
+ latestAttemptId: persisted.attemptId,
754
+ latestStructuredOutput: result.structuredOutput ?? null,
755
+ latestNotes: result.notes,
756
+ blockedReason: null,
757
+ failureClass: null,
758
+ completedAt: nowDate(),
759
+ }),
760
+ )
761
+ .output('after'),
762
+ )
763
+ const completedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, completedNodeRunRow, 'plan node run')
764
+
765
+ yield* emitEvent({
766
+ tx,
767
+ run: submission.run,
768
+ spec: submission.spec,
769
+ nodeId: completedNodeRun.nodeId,
770
+ attemptId: persisted.attemptId,
771
+ eventType: submission.validation.warnings.length > 0 ? 'node-partial' : 'node-completed',
772
+ fromStatus: submission.nodeRun.status,
773
+ toStatus: completedNodeRun.status,
774
+ message:
775
+ submission.validation.warnings.length > 0
776
+ ? `Node "${submission.nodeSpec.label}" completed with warnings.`
777
+ : `Node "${submission.nodeSpec.label}" completed successfully.`,
778
+ detail: { warningCount: submission.validation.warnings.length },
779
+ emittedBy,
780
+ capturedEvents: emittedEvents,
781
+ })
782
+
783
+ const synced = yield* fromPromise(() =>
784
+ syncRunGraph(context, {
785
+ tx,
786
+ run: submission.run,
787
+ spec: submission.spec,
788
+ nodeSpecs: submission.nodeSpecs,
789
+ nodeRuns: replaceNodeRunInList(persisted.withUpdatedNodeRuns, completedNodeRun),
790
+ artifacts: persisted.nextArtifacts,
791
+ emittedBy,
792
+ capturedEvents: emittedEvents,
793
+ }),
794
+ )
795
+
796
+ yield* saveAndAttachCheckpointEffect(context, {
797
+ tx,
798
+ run: synced.run,
799
+ spec: submission.spec,
800
+ nodeRuns: synced.nodeRuns,
801
+ artifacts: synced.artifacts,
802
+ sequence: submission.nextCheckpointSequence,
803
+ reason: 'node-result-complete',
804
+ capturedEvents: emittedEvents,
805
+ })
806
+ })
807
+ }
808
+
809
+ function resolveApprovalForWaitingNodeEffect(
810
+ context: Pick<PlanExecutorContext, 'planApprovalService'>,
811
+ params: { approvalId?: string; nodeRun: PlanNodeRunRecord; nodeLabel: string },
812
+ ) {
813
+ return Effect.gen(function* () {
814
+ const approvalId = params.approvalId ?? null
815
+ const explicitApproval = approvalId
816
+ ? yield* context.planApprovalService
817
+ .getApprovalById(approvalId)
818
+ .pipe(Effect.mapError((cause) => new PlanExecutorInternalError({ message: toError(cause).message, cause })))
819
+ : null
820
+
821
+ if (approvalId && !explicitApproval) {
822
+ return yield* new NotFoundError({
823
+ resource: 'plan-approval',
824
+ id: approvalId,
825
+ message: `Approval "${approvalId}" does not exist.`,
826
+ })
827
+ }
828
+
829
+ if (
830
+ explicitApproval &&
831
+ recordIdToString(explicitApproval.nodeRunId, TABLES.PLAN_NODE_RUN) !==
832
+ recordIdToString(params.nodeRun.id, TABLES.PLAN_NODE_RUN)
833
+ ) {
834
+ return yield* new BadRequestError({
835
+ message: `Approval "${approvalId}" does not belong to node "${params.nodeLabel}".`,
836
+ })
837
+ }
838
+
839
+ if (explicitApproval && explicitApproval.status !== 'pending') {
840
+ return yield* new BadRequestError({ message: `Approval "${approvalId}" is no longer pending.` })
841
+ }
842
+
843
+ const approval =
844
+ explicitApproval ??
845
+ (yield* context.planApprovalService
846
+ .getPendingApprovalForNodeRun(params.nodeRun.id)
847
+ .pipe(Effect.mapError((cause) => new PlanExecutorInternalError({ message: toError(cause).message, cause }))))
848
+ if (!approval) {
849
+ return yield* new BadRequestError({ message: `No pending approval exists for node "${params.nodeLabel}".` })
850
+ }
851
+
852
+ return approval satisfies PlanApprovalRecord
853
+ })
854
+ }
855
+
856
+ function loadHumanNodeResponseContextEffect(
857
+ context: Pick<PlanExecutorContext, 'planApprovalService' | 'planRunService' | 'planValidatorService'>,
858
+ params: { threadId: RecordIdInput; approvalId?: string; response: HumanNodeResponsePayload },
859
+ ) {
860
+ return Effect.gen(function* () {
861
+ const run = yield* context.planRunService.getActiveRunRecord(params.threadId)
862
+ if (!run || run.status !== 'awaiting-human' || !run.waitingNodeId) {
863
+ return null
864
+ }
865
+
866
+ const responseComments = readHumanResponseComments(params.response)
867
+ const submittedResult: PlanNodeResultSubmission = {
868
+ structuredOutput: params.response,
869
+ artifacts: [],
870
+ notes: responseComments ?? 'Human response submitted.',
871
+ }
872
+ const waitingNodeId = run.waitingNodeId
873
+ const spec = yield* context.planRunService.getPlanSpecById(run.planSpecId)
874
+ const nodeSpecs = yield* context.planRunService.listNodeSpecs(spec.id)
875
+ const nodeSpec = nodeSpecs.find((candidate) => candidate.nodeId === waitingNodeId)
876
+ if (!nodeSpec) {
877
+ return yield* new NotFoundError({
878
+ resource: 'plan-node',
879
+ id: waitingNodeId,
880
+ message: `Waiting node "${waitingNodeId}" does not exist.`,
881
+ })
882
+ }
883
+
884
+ const nodeRun = yield* context.planRunService.getNodeRunByNodeId(run.id, waitingNodeId)
885
+ const approval = yield* resolveApprovalForWaitingNodeEffect(context, {
886
+ approvalId: params.approvalId,
887
+ nodeRun,
888
+ nodeLabel: nodeSpec.label,
889
+ })
890
+ const existingArtifacts = yield* context.planRunService.listArtifacts(run.id)
891
+ const nextCheckpointSequence = yield* context.planRunService.getNextCheckpointSequence(run.id)
892
+ const validation = context.planValidatorService.validateNodeResult({
893
+ draft: { schemas: spec.schemaRegistry },
894
+ node: toPlanNodeValidationSpec(nodeSpec),
895
+ result: submittedResult,
896
+ })
897
+
898
+ return {
899
+ run,
900
+ spec,
901
+ nodeSpecs,
902
+ nodeSpec,
903
+ nodeRun,
904
+ approval,
905
+ existingArtifacts,
906
+ nextCheckpointSequence,
907
+ validation,
908
+ responseComments,
909
+ submittedResult,
910
+ } satisfies LoadedHumanNodeResponseSubmission
911
+ })
912
+ }
913
+
914
+ function persistHumanNodeResponseAttemptEffect(
915
+ context: Pick<PlanExecutorContext, 'planApprovalService' | 'planRunService'>,
916
+ params: {
917
+ tx: DatabaseTransaction
918
+ submission: LoadedHumanNodeResponseSubmission
919
+ respondedBy: string
920
+ approvalMessageId?: string
921
+ response: HumanNodeResponsePayload
922
+ },
923
+ ) {
924
+ const { tx, submission, respondedBy, approvalMessageId, response } = params
925
+
926
+ return Effect.gen(function* () {
927
+ const approvalStatus = deriveApprovalStatus(response)
928
+ yield* context.planApprovalService
929
+ .updateApprovalResponse({
930
+ tx,
931
+ approval: submission.approval,
932
+ status: approvalStatus,
933
+ response,
934
+ respondedBy,
935
+ approvalMessageId,
936
+ comments: submission.responseComments,
937
+ requiredEdits: readHumanRequiredEdits(response),
938
+ })
939
+ .pipe(Effect.mapError((cause) => new PlanExecutorInternalError({ message: toError(cause).message, cause })))
940
+
941
+ const attempt = yield* createAttempt({
942
+ tx,
943
+ run: submission.run,
944
+ nodeRun: submission.nodeRun,
945
+ emittedBy: respondedBy,
946
+ result: submission.submittedResult,
947
+ status: submission.validation.blocking.length > 0 ? 'failed' : 'completed',
948
+ failureClass: submission.validation.failureClass,
949
+ })
950
+
951
+ const issues = yield* persistValidationIssues({
952
+ tx,
953
+ run: submission.run,
954
+ spec: submission.spec,
955
+ attemptId: attempt.id,
956
+ nodeId: submission.nodeRun.nodeId,
957
+ issues: [...submission.validation.blocking, ...submission.validation.warnings],
958
+ })
959
+
960
+ const updatedHumanAttemptRow = yield* fromPromise(() =>
961
+ tx
962
+ .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
963
+ .content({
964
+ runId: ensureRecordId(attempt.runId, TABLES.PLAN_RUN),
965
+ nodeRunId: ensureRecordId(attempt.nodeRunId, TABLES.PLAN_NODE_RUN),
966
+ nodeId: attempt.nodeId,
967
+ emittedBy: attempt.emittedBy,
968
+ status: attempt.status,
969
+ structuredOutput: response,
970
+ validationIssueIds: issues.map((issue: { id: RecordIdInput }) =>
971
+ ensureRecordId(issue.id, TABLES.PLAN_VALIDATION_ISSUE),
972
+ ),
973
+ ...(attempt.notes ? { notes: attempt.notes } : {}),
974
+ ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
975
+ })
976
+ .output('after'),
977
+ )
978
+ void (yield* parseRowOrFail(PlanNodeAttemptSchema, updatedHumanAttemptRow, 'plan node attempt'))
979
+
980
+ const nextNodeRunRow = yield* fromPromise(() =>
981
+ tx
982
+ .update(ensureRecordId(submission.nodeRun.id, TABLES.PLAN_NODE_RUN))
983
+ .content(
984
+ toNodeRunData(submission.nodeRun, {
985
+ status: submission.validation.blocking.length > 0 ? 'blocked' : 'completed',
986
+ attemptCount: submission.nodeRun.attemptCount + 1,
987
+ latestAttemptId: attempt.id,
988
+ latestStructuredOutput: response,
989
+ latestNotes: submission.responseComments ?? null,
990
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
991
+ failureClass: submission.validation.blocking.length > 0 ? submission.validation.failureClass : null,
992
+ ...(submission.validation.blocking.length > 0 ? {} : { completedAt: nowDate() }),
993
+ }),
994
+ )
995
+ .output('after'),
996
+ )
997
+ const nextNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, nextNodeRunRow, 'plan node run')
998
+
999
+ const nodeRuns = replaceNodeRunInList(yield* context.planRunService.listNodeRuns(submission.run.id), nextNodeRun)
1000
+
1001
+ return { attemptId: attempt.id, approvalStatus, nextNodeRun, nodeRuns } satisfies PersistedHumanNodeResponseAttempt
1002
+ })
1003
+ }
1004
+
1005
+ function handleBlockedHumanNodeResponseEffect(
1006
+ context: PlanExecutorContext,
1007
+ params: {
1008
+ tx: DatabaseTransaction
1009
+ emittedEvents: CapturedPlanEvents
1010
+ submission: LoadedHumanNodeResponseSubmission
1011
+ persisted: PersistedHumanNodeResponseAttempt
1012
+ respondedBy: string
1013
+ },
1014
+ ) {
1015
+ const { tx, emittedEvents, submission, persisted, respondedBy } = params
1016
+
1017
+ return Effect.gen(function* () {
1018
+ const blockedRun = yield* replaceRun(tx, submission.run, {
1019
+ status: 'blocked',
1020
+ currentNodeId: persisted.nextNodeRun.nodeId,
1021
+ waitingNodeId: null,
1022
+ readyNodeIds: [],
1023
+ failureCount: submission.run.failureCount + 1,
1024
+ })
1025
+
1026
+ yield* emitEvent({
1027
+ tx,
1028
+ run: blockedRun,
1029
+ spec: submission.spec,
1030
+ nodeId: persisted.nextNodeRun.nodeId,
1031
+ attemptId: persisted.attemptId,
1032
+ approvalId: submission.approval.id,
1033
+ eventType: 'approval-resolved',
1034
+ fromStatus: submission.run.status,
1035
+ toStatus: blockedRun.status,
1036
+ message: `Human response for node "${submission.nodeSpec.label}" blocked execution.`,
1037
+ detail: { issues: submission.validation.blocking.map((issue) => issue.code) },
1038
+ emittedBy: respondedBy,
1039
+ capturedEvents: emittedEvents,
1040
+ })
1041
+
1042
+ yield* saveAndAttachCheckpointEffect(context, {
1043
+ tx,
1044
+ run: blockedRun,
1045
+ spec: submission.spec,
1046
+ nodeRuns: persisted.nodeRuns,
1047
+ artifacts: submission.existingArtifacts,
1048
+ sequence: submission.nextCheckpointSequence,
1049
+ reason: 'human-response-blocked',
1050
+ capturedEvents: emittedEvents,
1051
+ })
1052
+
1053
+ const latestRun = yield* context.planRunService.getRunById(submission.run.id)
1054
+ return yield* serializeRunFull(context.planRunService, latestRun)
1055
+ })
1056
+ }
1057
+
1058
+ function handleAcceptedHumanNodeResponseEffect(
1059
+ context: PlanExecutorContext,
1060
+ params: {
1061
+ tx: DatabaseTransaction
1062
+ emittedEvents: CapturedPlanEvents
1063
+ submission: LoadedHumanNodeResponseSubmission
1064
+ persisted: PersistedHumanNodeResponseAttempt
1065
+ respondedBy: string
1066
+ },
1067
+ ) {
1068
+ const { tx, emittedEvents, submission, persisted, respondedBy } = params
1069
+
1070
+ return Effect.gen(function* () {
1071
+ const synced = yield* fromPromise(() =>
1072
+ syncRunGraph(context, {
1073
+ tx,
1074
+ run: submission.run,
1075
+ spec: submission.spec,
1076
+ nodeSpecs: submission.nodeSpecs,
1077
+ nodeRuns: persisted.nodeRuns,
1078
+ artifacts: submission.existingArtifacts,
1079
+ emittedBy: respondedBy,
1080
+ capturedEvents: emittedEvents,
1081
+ }),
1082
+ )
1083
+
1084
+ yield* emitEvent({
1085
+ tx,
1086
+ run: synced.run,
1087
+ spec: submission.spec,
1088
+ nodeId: persisted.nextNodeRun.nodeId,
1089
+ attemptId: persisted.attemptId,
1090
+ approvalId: submission.approval.id,
1091
+ eventType: 'approval-resolved',
1092
+ fromStatus: submission.run.status,
1093
+ toStatus: synced.run.status,
1094
+ message: `Human response for node "${submission.nodeSpec.label}" accepted.`,
1095
+ detail: { approvalStatus: persisted.approvalStatus },
1096
+ emittedBy: respondedBy,
1097
+ capturedEvents: emittedEvents,
1098
+ })
1099
+
1100
+ yield* saveAndAttachCheckpointEffect(context, {
1101
+ tx,
1102
+ run: synced.run,
1103
+ spec: submission.spec,
1104
+ nodeRuns: synced.nodeRuns,
1105
+ artifacts: synced.artifacts,
1106
+ sequence: submission.nextCheckpointSequence,
1107
+ reason: 'human-response-complete',
1108
+ capturedEvents: emittedEvents,
1109
+ })
1110
+
1111
+ const latestRun = yield* context.planRunService.getRunById(submission.run.id)
1112
+ return yield* serializeRunFull(context.planRunService, latestRun)
1113
+ })
1114
+ }
1115
+
1116
+ const submitNodeResultEffect = Effect.fn('PlanExecutor.submitNodeResult')(function* (
1117
+ context: PlanExecutorContext,
1118
+ params: {
1119
+ threadId: RecordIdInput
1120
+ runId: string
1121
+ nodeId: string
1122
+ emittedBy: string
1123
+ result: PlanNodeResultSubmission
1124
+ },
1125
+ ) {
1126
+ const {
1127
+ databaseService,
1128
+ generatedDocumentStorageService,
1129
+ planCompletionSideEffects,
1130
+ planEventDeliveryService,
1131
+ planRunService,
1132
+ } = context
1133
+
1134
+ const submission = yield* loadNodeResultSubmissionContextEffect(context, params)
1135
+ const publishedArtifactStorageKeys: string[] = []
1136
+
1137
+ yield* withTransactionAndEventsEffect({
1138
+ db: databaseService,
1139
+ planEventDeliveryService,
1140
+ run: (tx, emittedEvents) =>
1141
+ Effect.gen(function* () {
1142
+ const persisted = yield* persistNodeResultAttemptEffect(context, {
1143
+ tx,
1144
+ submission,
1145
+ nodeId: params.nodeId,
1146
+ emittedBy: params.emittedBy,
1147
+ result: params.result,
1148
+ publishedArtifactStorageKeys,
1149
+ })
1150
+
1151
+ if (submission.validation.blocking.length > 0) {
1152
+ return yield* handleBlockedNodeResultEffect(context, {
1153
+ tx,
1154
+ emittedEvents,
1155
+ submission,
1156
+ persisted,
1157
+ emittedBy: params.emittedBy,
1158
+ })
1159
+ }
1160
+
1161
+ return yield* handleSuccessfulNodeResultEffect(context, {
1162
+ tx,
1163
+ emittedEvents,
1164
+ submission,
1165
+ persisted,
1166
+ emittedBy: params.emittedBy,
1167
+ result: params.result,
1168
+ })
1169
+ }),
1170
+ }).pipe(
1171
+ Effect.withSpan('PlanExecutor.submitNodeResult.persistTransaction'),
1172
+ Effect.tapError(() =>
1173
+ Effect.forEach(publishedArtifactStorageKeys, (storageKey) =>
1174
+ generatedDocumentStorageService.deleteTextArtifact(storageKey).pipe(Effect.ignore),
1175
+ ).pipe(Effect.asVoid),
1176
+ ),
1177
+ )
1178
+
1179
+ const orgId = recordIdToString(submission.run.organizationId, TABLES.ORGANIZATION)
1180
+ const runIdStr = recordIdToString(submission.run.id, TABLES.PLAN_RUN)
1181
+
1182
+ const background = yield* BackgroundWorkService
1183
+
1184
+ yield* background.run(
1185
+ fromPromise(() =>
1186
+ planCompletionSideEffects.runPlanNodeCompletionSideEffects({
1187
+ runId: runIdStr,
1188
+ organizationId: orgId,
1189
+ nodeId: params.nodeId,
1190
+ nodeLabel: submission.nodeSpec.label,
1191
+ nodeOwnerRef: submission.nodeSpec.owner.ref,
1192
+ nodeOwnerType: submission.nodeSpec.owner.executorType,
1193
+ nodeType: submission.nodeSpec.type,
1194
+ nodeStartedAt: submission.nodeRun.startedAt,
1195
+ nodeAttemptCount: submission.nodeRun.attemptCount + 1,
1196
+ artifactCount: params.result.artifacts.length,
1197
+ validationIssues: [...submission.validation.blocking, ...submission.validation.warnings],
1198
+ }),
1199
+ ).pipe(
1200
+ Effect.catchTag('PlanExecutorInternalError', (error) =>
1201
+ Effect.sync(() => {
1202
+ aiLogger.warn`Failed to record node completion metrics for run ${runIdStr} node ${params.nodeId}: ${error.message}`
1203
+ }),
1204
+ ),
1205
+ ),
1206
+ 'plan-executor.recordNodeCompletionMetrics',
1207
+ )
1208
+
1209
+ const updatedRun = yield* planRunService.getRunById(submission.run.id)
1210
+ if (updatedRun.status === 'completed') {
1211
+ yield* background.run(
1212
+ fromPromise(() =>
1213
+ planCompletionSideEffects.runPlanCompletionSideEffectsSafely({ runId: runIdStr, organizationId: orgId }),
1214
+ ).pipe(
1215
+ Effect.catchTag('PlanExecutorInternalError', (error) =>
1216
+ Effect.sync(() => {
1217
+ aiLogger.warn`Plan completion side-effects failed for run ${runIdStr}: ${error.message}`
1218
+ }),
1219
+ ),
1220
+ ),
1221
+ 'plan-executor.runPlanCompletionSideEffects',
1222
+ )
1223
+ }
1224
+
1225
+ const snapshot = yield* serializeRunFull(planRunService, updatedRun)
1226
+
1227
+ return buildExecutionPlanToolResult({
1228
+ action: 'node-result-submitted',
1229
+ plan: snapshot,
1230
+ message: `Submitted result for node "${submission.nodeSpec.label}".`,
1231
+ })
1232
+ })
1233
+
1234
+ /** @lintignore */
1235
+ function submitNodeResult(
1236
+ context: PlanExecutorContext,
1237
+ params: {
1238
+ threadId: RecordIdInput
1239
+ runId: string
1240
+ nodeId: string
1241
+ emittedBy: string
1242
+ result: PlanNodeResultSubmission
1243
+ },
1244
+ ): Promise<ExecutionPlanToolResultData> {
1245
+ return submitNodeResultEffect(context, params).pipe(runPromise)
1246
+ }
1247
+
1248
+ const submitHumanNodeResponseEffect = Effect.fn('PlanExecutor.submitHumanNodeResponse')(function* (
1249
+ context: PlanExecutorContext,
1250
+ params: {
1251
+ threadId: RecordIdInput
1252
+ approvalId?: string
1253
+ respondedBy: string
1254
+ response: HumanNodeResponsePayload
1255
+ approvalMessageId?: string
1256
+ },
1257
+ ) {
1258
+ const { databaseService, planEventDeliveryService } = context
1259
+
1260
+ const submission = yield* loadHumanNodeResponseContextEffect(context, params)
1261
+ if (!submission) {
1262
+ return null
1263
+ }
1264
+
1265
+ return yield* withTransactionAndEventsEffect({
1266
+ db: databaseService,
1267
+ planEventDeliveryService,
1268
+ run: (tx, emittedEvents) =>
1269
+ Effect.gen(function* () {
1270
+ const persisted = yield* persistHumanNodeResponseAttemptEffect(context, {
1271
+ tx,
1272
+ submission,
1273
+ respondedBy: params.respondedBy,
1274
+ approvalMessageId: params.approvalMessageId,
1275
+ response: params.response,
1276
+ })
1277
+
1278
+ if (submission.validation.blocking.length > 0) {
1279
+ return yield* handleBlockedHumanNodeResponseEffect(context, {
1280
+ tx,
1281
+ emittedEvents,
1282
+ submission,
1283
+ persisted,
1284
+ respondedBy: params.respondedBy,
1285
+ })
1286
+ }
1287
+
1288
+ return yield* handleAcceptedHumanNodeResponseEffect(context, {
1289
+ tx,
1290
+ emittedEvents,
1291
+ submission,
1292
+ persisted,
1293
+ respondedBy: params.respondedBy,
1294
+ })
1295
+ }),
1296
+ }).pipe(Effect.withSpan('PlanExecutor.submitHumanNodeResponse.persistTransaction'))
1297
+ })
1298
+
1299
+ /** @lintignore */
1300
+ function submitHumanNodeResponse(
1301
+ context: PlanExecutorContext,
1302
+ params: {
1303
+ threadId: RecordIdInput
1304
+ approvalId?: string
1305
+ respondedBy: string
1306
+ response: HumanNodeResponsePayload
1307
+ approvalMessageId?: string
1308
+ },
1309
+ ): Promise<SerializableExecutionPlan | null> {
1310
+ return submitHumanNodeResponseEffect(context, params).pipe(runPromise)
1311
+ }
1312
+
1313
+ const resumeRunEffect = Effect.fn('PlanExecutor.resumeRun')(function* (
1314
+ context: PlanExecutorContext,
1315
+ params: { threadId: RecordIdInput; runId: string; emittedBy: string },
1316
+ ) {
1317
+ const { databaseService, planEventDeliveryService, planRunService } = context
1318
+
1319
+ const run = yield* planRunService.getRunById(params.runId)
1320
+ const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1321
+ const [nodeSpecs, nodeRuns, artifacts, latestCheckpoint, nextCheckpointSequence] = yield* Effect.all([
1322
+ planRunService.listNodeSpecs(spec.id),
1323
+ planRunService.listNodeRuns(run.id),
1324
+ planRunService.listArtifacts(run.id),
1325
+ planRunService.getLatestCheckpoint(run.id),
1326
+ planRunService.getNextCheckpointSequence(run.id),
1327
+ ]).pipe(Effect.withSpan('PlanExecutor.resumeRun.loadRunGraph'))
1328
+
1329
+ const snapshot = yield* withTransactionAndEventsEffect({
1330
+ db: databaseService,
1331
+ planEventDeliveryService,
1332
+ run: (tx, emittedEvents) =>
1333
+ Effect.gen(function* () {
1334
+ let currentNodeRuns = [...nodeRuns]
1335
+ for (const currentNodeRun of currentNodeRuns.filter((candidate) => candidate.status === 'running')) {
1336
+ const resetNodeRunRow = yield* fromPromise(() =>
1337
+ tx
1338
+ .update(ensureRecordId(currentNodeRun.id, TABLES.PLAN_NODE_RUN))
1339
+ .content(
1340
+ toNodeRunData(currentNodeRun, {
1341
+ status: 'ready',
1342
+ readyAt: nowDate(),
1343
+ startedAt: currentNodeRun.startedAt ?? nowDate(),
1344
+ }),
1345
+ )
1346
+ .output('after'),
1347
+ )
1348
+ const resetNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, resetNodeRunRow, 'plan node run')
1349
+ currentNodeRuns = currentNodeRuns.map((candidate) =>
1350
+ candidate.nodeId === resetNodeRun.nodeId ? resetNodeRun : candidate,
1351
+ )
1352
+ }
1353
+
1354
+ const resetRun = yield* replaceRun(tx, run, {
1355
+ status: run.status === 'awaiting-human' ? 'awaiting-human' : 'running',
1356
+ currentNodeId: run.status === 'awaiting-human' ? (run.currentNodeId ?? null) : null,
1357
+ waitingNodeId: run.status === 'awaiting-human' ? (run.waitingNodeId ?? null) : null,
1358
+ readyNodeIds: currentNodeRuns
1359
+ .filter((candidate) => candidate.status === 'ready')
1360
+ .map((candidate) => candidate.nodeId),
1361
+ })
1362
+
1363
+ yield* emitEvent({
1364
+ tx,
1365
+ run: resetRun,
1366
+ spec,
1367
+ eventType: 'run-resumed',
1368
+ fromStatus: run.status,
1369
+ toStatus: resetRun.status,
1370
+ message: `Run "${spec.title}" resumed from the latest checkpoint.`,
1371
+ detail: latestCheckpoint
1372
+ ? { checkpointId: recordIdToString(latestCheckpoint.id, TABLES.PLAN_CHECKPOINT) }
1373
+ : {},
1374
+ emittedBy: params.emittedBy,
1375
+ capturedEvents: emittedEvents,
1376
+ })
1377
+
1378
+ const synced =
1379
+ resetRun.status === 'awaiting-human'
1380
+ ? { run: resetRun, nodeRuns: currentNodeRuns, artifacts }
1381
+ : yield* fromPromise(() =>
1382
+ syncRunGraph(context, {
1383
+ tx,
1384
+ run: resetRun,
1385
+ spec,
1386
+ nodeSpecs,
1387
+ nodeRuns: currentNodeRuns,
1388
+ artifacts,
1389
+ emittedBy: params.emittedBy,
1390
+ capturedEvents: emittedEvents,
1391
+ }),
1392
+ )
1393
+
1394
+ const checkpoint = yield* saveCheckpointWithContext(context, {
1395
+ tx,
1396
+ run: synced.run,
1397
+ spec,
1398
+ nodeRuns: synced.nodeRuns,
1399
+ artifacts: synced.artifacts,
1400
+ sequence: nextCheckpointSequence,
1401
+ reason: 'run-resumed',
1402
+ capturedEvents: emittedEvents,
1403
+ })
1404
+ yield* attachCheckpoint(tx, synced.run, checkpoint)
1405
+ const latestRun = yield* planRunService.getRunById(run.id)
1406
+ return yield* serializeRunFull(planRunService, latestRun)
1407
+ }),
1408
+ }).pipe(Effect.withSpan('PlanExecutor.resumeRun.persistTransaction'))
1409
+
1410
+ return buildExecutionPlanToolResult({
1411
+ action: 'run-resumed',
1412
+ plan: snapshot,
1413
+ message: `Resumed execution run "${snapshot.title}".`,
1414
+ })
1415
+ })
1416
+
1417
+ /** @lintignore */
1418
+ function resumeRun(
1419
+ context: PlanExecutorContext,
1420
+ params: { threadId: RecordIdInput; runId: string; emittedBy: string },
1421
+ ): Promise<ExecutionPlanToolResultData> {
1422
+ return resumeRunEffect(context, params).pipe(runPromise)
1423
+ }
1424
+
1425
+ const transitionNodeToRunningEffect = Effect.fn('PlanExecutor.transitionNodeToRunning')(function* (
1426
+ context: PlanExecutorContext,
1427
+ params: { runId: string; nodeId: string },
1428
+ ) {
1429
+ const { databaseService, planRunService } = context
1430
+
1431
+ const run = yield* planRunService.getRunById(params.runId)
1432
+ const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1433
+ if (nodeRun.status !== 'ready') return
1434
+
1435
+ yield* withDatabaseTransactionEffect(databaseService, (tx) =>
1436
+ Effect.gen(function* () {
1437
+ const runningNodeRunRow = yield* fromPromise(() =>
1438
+ tx
1439
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1440
+ .content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? nowDate() }))
1441
+ .output('after'),
1442
+ )
1443
+ const runningNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, runningNodeRunRow, 'plan node run')
1444
+
1445
+ const nodeRuns = yield* planRunService.listNodeRuns(run.id)
1446
+ yield* replaceRun(tx, run, {
1447
+ status: 'running',
1448
+ currentNodeId: runningNodeRun.nodeId,
1449
+ waitingNodeId: null,
1450
+ readyNodeIds: nodeRuns
1451
+ .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
1452
+ .map((candidate) => candidate.nodeId),
1453
+ })
1454
+ }),
1455
+ ).pipe(Effect.withSpan('PlanExecutor.transitionNodeToRunning.persistTransaction'))
1456
+ })
1457
+
1458
+ /** @lintignore */
1459
+ function transitionNodeToRunning(
1460
+ context: PlanExecutorContext,
1461
+ params: { runId: string; nodeId: string },
1462
+ ): Promise<void> {
1463
+ return transitionNodeToRunningEffect(context, params).pipe(runPromise)
1464
+ }
1465
+
1466
+ const blockNodeOnDispatchFailureEffect = Effect.fn('PlanExecutor.blockNodeOnDispatchFailure')(function* (
1467
+ context: PlanExecutorContext,
1468
+ params: {
1469
+ threadId: RecordIdInput
1470
+ runId: string
1471
+ nodeId: string
1472
+ emittedBy: string
1473
+ message: string
1474
+ failureClass: PlanFailureClass
1475
+ },
1476
+ ) {
1477
+ const { databaseService, planEventDeliveryService, planRunService } = context
1478
+
1479
+ const run = yield* planRunService.getRunById(params.runId)
1480
+ const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1481
+ const [nodeSpec, nodeRun, artifacts, nextCheckpointSequence] = yield* Effect.all([
1482
+ planRunService.getNodeSpecByNodeId(spec.id, params.nodeId),
1483
+ planRunService.getNodeRunByNodeId(run.id, params.nodeId),
1484
+ planRunService.listArtifacts(run.id),
1485
+ planRunService.getNextCheckpointSequence(run.id),
1486
+ ]).pipe(Effect.withSpan('PlanExecutor.blockNodeOnDispatchFailure.loadNodeContext'))
1487
+
1488
+ return yield* withTransactionAndEventsEffect({
1489
+ db: databaseService,
1490
+ planEventDeliveryService,
1491
+ run: (tx, emittedEvents) =>
1492
+ Effect.gen(function* () {
1493
+ let blockedNodeRun: PlanNodeRunRecord
1494
+ if (nodeRun.status === 'blocked') {
1495
+ blockedNodeRun = nodeRun
1496
+ } else {
1497
+ const blockedNodeRunRow = yield* fromPromise(() =>
1498
+ tx
1499
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1500
+ .content(
1501
+ toNodeRunData(nodeRun, {
1502
+ status: 'blocked',
1503
+ blockedReason: params.message,
1504
+ failureClass: params.failureClass,
1505
+ }),
1506
+ )
1507
+ .output('after'),
1508
+ )
1509
+ blockedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, blockedNodeRunRow, 'plan node run')
1510
+ }
1511
+
1512
+ const blockedRun = yield* replaceRun(tx, run, {
1513
+ status: 'blocked',
1514
+ currentNodeId: blockedNodeRun.nodeId,
1515
+ waitingNodeId: null,
1516
+ readyNodeIds: [],
1517
+ failureCount: run.failureCount + 1,
1518
+ })
1519
+
1520
+ yield* emitEvent({
1521
+ tx,
1522
+ run: blockedRun,
1523
+ spec,
1524
+ nodeId: blockedNodeRun.nodeId,
1525
+ eventType: 'node-blocked',
1526
+ fromStatus: nodeRun.status,
1527
+ toStatus: blockedNodeRun.status,
1528
+ message: `Node "${nodeSpec.label}" failed during owner dispatch.`,
1529
+ detail: { failureClass: params.failureClass, phase: 'dispatch', error: params.message },
1530
+ emittedBy: params.emittedBy,
1531
+ capturedEvents: emittedEvents,
1532
+ })
1533
+
1534
+ const checkpointNodeRuns = (yield* planRunService.listNodeRuns(run.id)).map((candidate) =>
1535
+ candidate.nodeId === blockedNodeRun.nodeId ? blockedNodeRun : candidate,
1536
+ )
1537
+ const checkpoint = yield* saveCheckpointWithContext(context, {
1538
+ tx,
1539
+ run: blockedRun,
1540
+ spec,
1541
+ nodeRuns: checkpointNodeRuns,
1542
+ artifacts,
1543
+ sequence: nextCheckpointSequence,
1544
+ reason: 'owner-dispatch-failed',
1545
+ capturedEvents: emittedEvents,
1546
+ })
1547
+ yield* attachCheckpoint(tx, blockedRun, checkpoint)
1548
+ const latestRun = yield* planRunService.getRunById(run.id)
1549
+ return yield* serializeRunFull(planRunService, latestRun)
1550
+ }),
1551
+ }).pipe(Effect.withSpan('PlanExecutor.blockNodeOnDispatchFailure.persistTransaction'))
1552
+ })
1553
+
1554
+ /** @lintignore */
1555
+ function blockNodeOnDispatchFailure(
1556
+ context: PlanExecutorContext,
1557
+ params: {
1558
+ threadId: RecordIdInput
1559
+ runId: string
1560
+ nodeId: string
1561
+ emittedBy: string
1562
+ message: string
1563
+ failureClass: PlanFailureClass
1564
+ },
1565
+ ): Promise<SerializableExecutionPlan> {
1566
+ return blockNodeOnDispatchFailureEffect(context, params).pipe(runPromise)
1567
+ }
1568
+
1569
+ const promoteDelayedNodeEffect = Effect.fn('PlanExecutor.promoteDelayedNode')(function* (
1570
+ context: PlanExecutorContext,
1571
+ params: { runId: string; nodeId: string; emittedBy: string },
1572
+ ) {
1573
+ const { databaseService, planEventDeliveryService, planRunService } = context
1574
+
1575
+ const run = yield* planRunService.getRunById(params.runId)
1576
+ if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
1577
+ return
1578
+ }
1579
+
1580
+ const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1581
+ const nodeSpecs = yield* planRunService.listNodeSpecs(spec.id)
1582
+ const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1583
+ if (nodeRun.status !== 'scheduled') return
1584
+
1585
+ const [nodeRuns, artifacts, nextCheckpointSequence] = yield* Effect.all([
1586
+ planRunService.listNodeRuns(run.id),
1587
+ planRunService.listArtifacts(run.id),
1588
+ planRunService.getNextCheckpointSequence(run.id),
1589
+ ]).pipe(Effect.withSpan('PlanExecutor.promoteDelayedNode.loadRunContext'))
1590
+
1591
+ yield* withTransactionAndEventsEffect({
1592
+ db: databaseService,
1593
+ planEventDeliveryService,
1594
+ run: (tx, emittedEvents) =>
1595
+ Effect.gen(function* () {
1596
+ const readyNodeRunRow = yield* fromPromise(() =>
1597
+ tx
1598
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1599
+ .content(toNodeRunData(nodeRun, { status: 'ready', readyAt: nowDate() }))
1600
+ .output('after'),
1601
+ )
1602
+ const readyNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, readyNodeRunRow, 'plan node run')
1603
+
1604
+ const updatedNodeRuns = nodeRuns.map((candidate) =>
1605
+ candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
1606
+ )
1607
+
1608
+ const currentNodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
1609
+ yield* emitEvent({
1610
+ tx,
1611
+ run,
1612
+ spec,
1613
+ nodeId: readyNodeRun.nodeId,
1614
+ eventType: 'node-ready',
1615
+ fromStatus: nodeRun.status,
1616
+ toStatus: readyNodeRun.status,
1617
+ message: `Node "${currentNodeSpec?.label ?? params.nodeId}" promoted to ready after delay.`,
1618
+ emittedBy: params.emittedBy,
1619
+ capturedEvents: emittedEvents,
1620
+ })
1621
+
1622
+ const synced = yield* fromPromise(() =>
1623
+ syncRunGraph(context, {
1624
+ tx,
1625
+ run,
1626
+ spec,
1627
+ nodeSpecs,
1628
+ nodeRuns: updatedNodeRuns,
1629
+ artifacts,
1630
+ emittedBy: params.emittedBy,
1631
+ capturedEvents: emittedEvents,
1632
+ }),
1633
+ )
1634
+
1635
+ const checkpoint = yield* saveCheckpointWithContext(context, {
1636
+ tx,
1637
+ run: synced.run,
1638
+ spec,
1639
+ nodeRuns: synced.nodeRuns,
1640
+ artifacts: synced.artifacts,
1641
+ sequence: nextCheckpointSequence,
1642
+ reason: 'delayed-node-promoted',
1643
+ capturedEvents: emittedEvents,
1644
+ })
1645
+ yield* attachCheckpoint(tx, synced.run, checkpoint)
1646
+ }),
1647
+ }).pipe(Effect.withSpan('PlanExecutor.promoteDelayedNode.persistTransaction'))
1648
+ })
1649
+
1650
+ /** @lintignore */
1651
+ function promoteDelayedNode(
1652
+ context: PlanExecutorContext,
1653
+ params: { runId: string; nodeId: string; emittedBy: string },
1654
+ ): Promise<void> {
1655
+ return promoteDelayedNodeEffect(context, params).pipe(runPromise)
1656
+ }
1657
+
1658
+ export function makePlanExecutorService(deps: PlanExecutorDeps) {
1659
+ const context: PlanExecutorContext = {
1660
+ databaseService: deps.db,
1661
+ generatedDocumentStorageService: deps.storage,
1662
+ artifactService: deps.artifactService,
1663
+ feedbackLoopService: deps.feedbackLoopService,
1664
+ institutionalMemoryService: deps.institutionalMemoryService,
1665
+ planApprovalService: deps.planApprovalService,
1666
+ planArtifactService: deps.planArtifactService,
1667
+ planCheckpointService: deps.planCheckpointService,
1668
+ planCoordinationService: deps.planCoordinationService,
1669
+ planEventDeliveryService: deps.planEventDeliveryService,
1670
+ planRunService: deps.planRunService,
1671
+ planSchedulerService: deps.planSchedulerService,
1672
+ planValidatorService: deps.planValidatorService,
1673
+ qualityMetricsService: deps.qualityMetricsService,
1674
+ planCompletionSideEffects: makePlanCompletionSideEffects({
1675
+ databaseService: deps.db,
1676
+ feedbackLoopService: deps.feedbackLoopService,
1677
+ institutionalMemoryService: deps.institutionalMemoryService,
1678
+ planEventDeliveryService: deps.planEventDeliveryService,
1679
+ planRunService: deps.planRunService,
1680
+ qualityMetricsService: deps.qualityMetricsService,
1681
+ }),
1682
+ }
1683
+
1684
+ return {
1685
+ submitNodeResult: (params: Parameters<typeof submitNodeResult>[1]) => submitNodeResult(context, params),
1686
+ submitNodeResultEffect: (params: Parameters<typeof submitNodeResultEffect>[1]) =>
1687
+ submitNodeResultEffect(context, params),
1688
+ submitHumanNodeResponse: (params: Parameters<typeof submitHumanNodeResponse>[1]) =>
1689
+ submitHumanNodeResponse(context, params),
1690
+ submitHumanNodeResponseEffect: (params: Parameters<typeof submitHumanNodeResponseEffect>[1]) =>
1691
+ submitHumanNodeResponseEffect(context, params),
1692
+ resumeRun: (params: Parameters<typeof resumeRun>[1]) => resumeRun(context, params),
1693
+ resumeRunEffect: (params: Parameters<typeof resumeRunEffect>[1]) => resumeRunEffect(context, params),
1694
+ transitionNodeToRunning: (params: Parameters<typeof transitionNodeToRunning>[1]) =>
1695
+ transitionNodeToRunning(context, params),
1696
+ transitionNodeToRunningEffect: (params: Parameters<typeof transitionNodeToRunningEffect>[1]) =>
1697
+ transitionNodeToRunningEffect(context, params),
1698
+ blockNodeOnDispatchFailure: (params: Parameters<typeof blockNodeOnDispatchFailure>[1]) =>
1699
+ blockNodeOnDispatchFailure(context, params),
1700
+ blockNodeOnDispatchFailureEffect: (params: Parameters<typeof blockNodeOnDispatchFailureEffect>[1]) =>
1701
+ blockNodeOnDispatchFailureEffect(context, params),
1702
+ promoteDelayedNode: (params: Parameters<typeof promoteDelayedNode>[1]) => promoteDelayedNode(context, params),
1703
+ promoteDelayedNodeEffect: (params: Parameters<typeof promoteDelayedNodeEffect>[1]) =>
1704
+ promoteDelayedNodeEffect(context, params),
1705
+ syncRunGraph: (params: Parameters<typeof syncRunGraph>[1]) => syncRunGraph(context, params),
1706
+ resolveFailureAction,
1707
+ buildResolvedInput,
1708
+ }
1709
+ }
1710
+
1711
+ export class PlanExecutorServiceTag extends Context.Service<PlanExecutorServiceTag, PlanExecutorService>()(
1712
+ '@lota-sdk/core/PlanExecutorService',
1713
+ ) {}
1714
+
1715
+ export const PlanExecutorServiceLive = Layer.effect(
1716
+ PlanExecutorServiceTag,
1717
+ Effect.gen(function* () {
1718
+ const db = yield* DatabaseServiceTag
1719
+ const storage = yield* GeneratedDocumentStorageServiceTag
1720
+ return makePlanExecutorService({
1721
+ db,
1722
+ storage,
1723
+ artifactService: yield* ArtifactServiceTag,
1724
+ feedbackLoopService: yield* FeedbackLoopServiceTag,
1725
+ institutionalMemoryService: yield* InstitutionalMemoryServiceTag,
1726
+ planApprovalService: yield* PlanApprovalServiceTag,
1727
+ planArtifactService: yield* PlanArtifactServiceTag,
1728
+ planCheckpointService: yield* PlanCheckpointServiceTag,
1729
+ planCoordinationService: yield* PlanCoordinationServiceTag,
1730
+ planEventDeliveryService: yield* PlanEventDeliveryServiceTag,
1731
+ planRunService: yield* PlanRunServiceTag,
1732
+ planSchedulerService: yield* PlanSchedulerServiceTag,
1733
+ planValidatorService: yield* PlanValidatorServiceTag,
1734
+ qualityMetricsService: yield* QualityMetricsServiceTag,
1735
+ })
1736
+ }),
1737
+ )