@lota-sdk/core 0.4.9 → 0.4.11

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 (182) hide show
  1. package/package.json +2 -2
  2. package/src/ai/embedding-cache.ts +3 -1
  3. package/src/ai-gateway/ai-gateway.ts +164 -82
  4. package/src/ai-gateway/index.ts +16 -1
  5. package/src/config/agent-defaults.ts +4 -107
  6. package/src/config/agent-types.ts +1 -1
  7. package/src/config/background-processing.ts +1 -1
  8. package/src/config/index.ts +0 -1
  9. package/src/config/logger.ts +22 -25
  10. package/src/config/thread-defaults.ts +1 -10
  11. package/src/create-runtime.ts +145 -670
  12. package/src/db/base.service.ts +30 -38
  13. package/src/db/memory-query-builder.ts +2 -1
  14. package/src/db/memory-store.ts +29 -20
  15. package/src/db/memory.ts +188 -195
  16. package/src/db/service-normalization.ts +97 -64
  17. package/src/db/service.ts +496 -384
  18. package/src/db/startup.ts +30 -19
  19. package/src/effect/helpers.ts +30 -5
  20. package/src/effect/index.ts +7 -7
  21. package/src/effect/layers.ts +75 -72
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -71
  24. package/src/index.ts +13 -12
  25. package/src/queues/autonomous-job.queue.ts +177 -143
  26. package/src/queues/context-compaction.queue.ts +41 -39
  27. package/src/queues/delayed-node-promotion.queue.ts +61 -42
  28. package/src/queues/document-processor.queue.ts +5 -3
  29. package/src/queues/index.ts +1 -0
  30. package/src/queues/memory-consolidation.queue.ts +79 -53
  31. package/src/queues/organization-learning.queue.ts +70 -33
  32. package/src/queues/plan-agent-heartbeat.queue.ts +111 -83
  33. package/src/queues/plan-scheduler.queue.ts +101 -97
  34. package/src/queues/post-chat-memory.queue.ts +56 -46
  35. package/src/queues/queue-factory.ts +146 -69
  36. package/src/queues/queues.service.ts +61 -0
  37. package/src/queues/title-generation.queue.ts +44 -44
  38. package/src/redis/connection.ts +181 -164
  39. package/src/redis/org-memory-lock.ts +24 -9
  40. package/src/redis/redis-lease-lock.ts +8 -1
  41. package/src/redis/stream-context.ts +17 -9
  42. package/src/runtime/agent-identity-overrides.ts +7 -3
  43. package/src/runtime/agent-runtime-policy.ts +10 -5
  44. package/src/runtime/agent-stream-helpers.ts +24 -15
  45. package/src/runtime/chat-run-orchestration.ts +1 -1
  46. package/src/runtime/context-compaction/context-compaction-runtime.ts +28 -32
  47. package/src/runtime/context-compaction/context-compaction.ts +131 -85
  48. package/src/runtime/domain-layer.ts +203 -0
  49. package/src/runtime/execution-plan-visibility.ts +5 -2
  50. package/src/runtime/graph-designer.ts +0 -14
  51. package/src/runtime/helper-model.ts +8 -4
  52. package/src/runtime/index.ts +1 -1
  53. package/src/runtime/indexed-repositories-policy.ts +2 -6
  54. package/src/runtime/memory/memory-block.ts +19 -9
  55. package/src/runtime/memory/memory-pipeline.ts +53 -66
  56. package/src/runtime/memory/memory-scope.ts +33 -29
  57. package/src/runtime/plugin-resolution.ts +58 -62
  58. package/src/runtime/post-turn-side-effects.ts +139 -161
  59. package/src/runtime/retrieval-adapters.ts +4 -4
  60. package/src/runtime/runtime-config.ts +3 -9
  61. package/src/runtime/runtime-extensions.ts +0 -43
  62. package/src/runtime/runtime-lifecycle.ts +124 -0
  63. package/src/runtime/runtime-services.ts +455 -0
  64. package/src/runtime/runtime-worker-registry.ts +113 -30
  65. package/src/runtime/social-chat/social-chat-agent-runner.ts +13 -8
  66. package/src/runtime/social-chat/social-chat-history.ts +24 -13
  67. package/src/runtime/social-chat/social-chat.ts +420 -369
  68. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +64 -57
  69. package/src/runtime/team-consultation/team-consultation-prompts.ts +11 -6
  70. package/src/runtime/thread-chat-helpers.ts +18 -9
  71. package/src/runtime/thread-turn-context.ts +28 -74
  72. package/src/runtime/turn-lifecycle.ts +6 -14
  73. package/src/services/agent-activity.service.ts +169 -176
  74. package/src/services/agent-executor.service.ts +207 -196
  75. package/src/services/artifact.service.ts +10 -5
  76. package/src/services/attachment.service.ts +16 -48
  77. package/src/services/autonomous-job.service.ts +81 -87
  78. package/src/services/background-work.service.ts +54 -0
  79. package/src/services/chat-run-registry.service.ts +3 -1
  80. package/src/services/context-compaction.service.ts +8 -10
  81. package/src/services/document-chunk.service.ts +8 -17
  82. package/src/services/execution-plan/execution-plan-graph.ts +122 -109
  83. package/src/services/execution-plan/execution-plan-schedule.ts +1 -15
  84. package/src/services/execution-plan/execution-plan.service.ts +68 -51
  85. package/src/services/feedback-loop.service.ts +1 -1
  86. package/src/services/global-orchestrator.service.ts +49 -15
  87. package/src/services/graph-full-routing.ts +49 -37
  88. package/src/services/index.ts +1 -0
  89. package/src/services/institutional-memory.service.ts +8 -17
  90. package/src/services/learned-skill.service.ts +38 -35
  91. package/src/services/memory/memory-conversation.ts +10 -5
  92. package/src/services/memory/memory-errors.ts +27 -0
  93. package/src/services/memory/memory-org-memory.ts +14 -3
  94. package/src/services/memory/memory-preseeded.ts +10 -4
  95. package/src/services/memory/memory-utils.ts +2 -1
  96. package/src/services/memory/memory.service.ts +37 -52
  97. package/src/services/memory/rerank.service.ts +3 -11
  98. package/src/services/monitoring-window.service.ts +1 -1
  99. package/src/services/mutating-approval.service.ts +1 -1
  100. package/src/services/node-workspace.service.ts +2 -2
  101. package/src/services/notification.service.ts +16 -4
  102. package/src/services/organization-member.service.ts +1 -1
  103. package/src/services/organization.service.ts +34 -51
  104. package/src/services/ownership-dispatcher.service.ts +148 -95
  105. package/src/services/plan/plan-agent-heartbeat.service.ts +30 -16
  106. package/src/services/plan/plan-agent-query.service.ts +13 -9
  107. package/src/services/plan/plan-approval.service.ts +52 -48
  108. package/src/services/plan/plan-artifact.service.ts +2 -2
  109. package/src/services/plan/plan-builder.service.ts +2 -2
  110. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  111. package/src/services/plan/plan-compiler.service.ts +1 -1
  112. package/src/services/plan/plan-completion-side-effects.ts +99 -113
  113. package/src/services/plan/plan-coordination.service.ts +1 -1
  114. package/src/services/plan/plan-cycle.service.ts +171 -202
  115. package/src/services/plan/plan-deadline.service.ts +304 -307
  116. package/src/services/plan/plan-event-delivery.service.ts +84 -72
  117. package/src/services/plan/plan-executor-context.ts +2 -0
  118. package/src/services/plan/plan-executor-graph.ts +375 -353
  119. package/src/services/plan/plan-executor-helpers.ts +60 -75
  120. package/src/services/plan/plan-executor.service.ts +494 -489
  121. package/src/services/plan/plan-run.service.ts +12 -19
  122. package/src/services/plan/plan-scheduler.service.ts +89 -82
  123. package/src/services/plan/plan-template.service.ts +1 -1
  124. package/src/services/plan/plan-transaction-events.ts +8 -5
  125. package/src/services/plan/plan-validator.service.ts +1 -1
  126. package/src/services/plan/plan-workspace.service.ts +17 -11
  127. package/src/services/plugin-executor.service.ts +26 -21
  128. package/src/services/quality-metrics.service.ts +1 -1
  129. package/src/services/queue-job.service.ts +8 -17
  130. package/src/services/recent-activity-title.service.ts +22 -10
  131. package/src/services/recent-activity.service.ts +1 -1
  132. package/src/services/skill-resolver.service.ts +1 -1
  133. package/src/services/social-chat-history.service.ts +37 -20
  134. package/src/services/system-executor.service.ts +25 -20
  135. package/src/services/thread/thread-bootstrap.ts +37 -19
  136. package/src/services/thread/thread-listing.ts +2 -1
  137. package/src/services/thread/thread-memory-block.ts +18 -5
  138. package/src/services/thread/thread-message.service.ts +30 -13
  139. package/src/services/thread/thread-title.service.ts +1 -1
  140. package/src/services/thread/thread-turn-execution.ts +87 -83
  141. package/src/services/thread/thread-turn-preparation.service.ts +65 -40
  142. package/src/services/thread/thread-turn-streaming.ts +32 -36
  143. package/src/services/thread/thread-turn.ts +43 -29
  144. package/src/services/thread/thread.service.ts +32 -8
  145. package/src/services/user.service.ts +1 -1
  146. package/src/services/write-intent-validator.service.ts +1 -1
  147. package/src/storage/attachment-storage.service.ts +7 -4
  148. package/src/storage/generated-document-storage.service.ts +1 -1
  149. package/src/system-agents/context-compaction.agent.ts +1 -1
  150. package/src/system-agents/helper-agent-options.ts +1 -1
  151. package/src/system-agents/memory-reranker.agent.ts +1 -1
  152. package/src/system-agents/memory.agent.ts +1 -1
  153. package/src/system-agents/recent-activity-title-refiner.agent.ts +9 -6
  154. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  155. package/src/system-agents/skill-extractor.agent.ts +1 -1
  156. package/src/system-agents/skill-manager.agent.ts +1 -1
  157. package/src/system-agents/thread-router.agent.ts +23 -20
  158. package/src/system-agents/title-generator.agent.ts +1 -1
  159. package/src/tools/execution-plan.tool.ts +36 -20
  160. package/src/tools/fetch-webpage.tool.ts +30 -22
  161. package/src/tools/firecrawl-client.ts +1 -6
  162. package/src/tools/plan-approval.tool.ts +9 -1
  163. package/src/tools/remember-memory.tool.ts +3 -6
  164. package/src/tools/research-topic.tool.ts +12 -3
  165. package/src/tools/search-web.tool.ts +26 -18
  166. package/src/tools/search.tool.ts +4 -5
  167. package/src/tools/team-think.tool.ts +139 -121
  168. package/src/utils/async.ts +15 -6
  169. package/src/utils/errors.ts +27 -15
  170. package/src/workers/bootstrap.ts +34 -58
  171. package/src/workers/memory-consolidation.worker.ts +4 -1
  172. package/src/workers/organization-learning.worker.ts +16 -3
  173. package/src/workers/regular-chat-memory-digest.helpers.ts +3 -4
  174. package/src/workers/regular-chat-memory-digest.runner.ts +46 -29
  175. package/src/workers/skill-extraction.runner.ts +13 -15
  176. package/src/workers/worker-utils.ts +14 -8
  177. package/src/config/search.ts +0 -3
  178. package/src/effect/awaitable-effect.ts +0 -87
  179. package/src/effect/runtime-ref.ts +0 -25
  180. package/src/effect/runtime.ts +0 -31
  181. package/src/redis/runtime-connection.ts +0 -10
  182. package/src/runtime/agent-types.ts +0 -1
@@ -1,11 +1,7 @@
1
- import type {
2
- ExecutionPlanToolResultData,
3
- PlanFailureClass,
4
- PlanNodeResultSubmission,
5
- SerializableExecutionPlan,
6
- } from '@lota-sdk/shared'
1
+ import type { PlanFailureClass, PlanNodeResultSubmission } from '@lota-sdk/shared'
7
2
  import { PlanNodeAttemptSchema, PlanNodeRunSchema } from '@lota-sdk/shared'
8
3
  import { Context, Schema, Effect, Layer } from 'effect'
4
+ import type { z } from 'zod'
9
5
 
10
6
  import { aiLogger } from '../../config/logger'
11
7
  import type { RecordIdInput } from '../../db/record-id'
@@ -14,12 +10,14 @@ import type { DatabaseTransaction } from '../../db/service'
14
10
  import { TABLES } from '../../db/tables'
15
11
  import { BadRequestError, NotFoundError } from '../../effect/errors'
16
12
  import { effectTryPromise as effectTryPromiseShared } from '../../effect/helpers'
17
- import { runPromise } from '../../effect/runtime'
18
13
  import { DatabaseServiceTag } from '../../effect/services'
14
+ import type { DelayedNodePromotionQueueRuntime } from '../../queues/delayed-node-promotion.queue'
15
+ import { LotaQueuesServiceTag } from '../../queues/queues.service'
19
16
  import { GeneratedDocumentStorageServiceTag } from '../../storage/generated-document-storage.service'
20
17
  import { nowDate } from '../../utils/date-time'
21
18
  import { toError } from '../../utils/errors'
22
19
  import { ArtifactServiceTag } from '../artifact.service'
20
+ import { BackgroundWorkService } from '../background-work.service'
23
21
  import { FeedbackLoopServiceTag } from '../feedback-loop.service'
24
22
  import { InstitutionalMemoryServiceTag } from '../institutional-memory.service'
25
23
  import { QualityMetricsServiceTag } from '../quality-metrics.service'
@@ -74,6 +72,7 @@ interface PlanExecutorDeps {
74
72
  planSchedulerService: Context.Service.Shape<typeof PlanSchedulerServiceTag>
75
73
  planValidatorService: Context.Service.Shape<typeof PlanValidatorServiceTag>
76
74
  qualityMetricsService: Context.Service.Shape<typeof QualityMetricsServiceTag>
75
+ delayedNodePromotionQueue: DelayedNodePromotionQueueRuntime
77
76
  }
78
77
 
79
78
  type PlanExecutorService = ReturnType<typeof makePlanExecutorService>
@@ -97,6 +96,17 @@ function fromPromise<A>(thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>)
97
96
  )
98
97
  }
99
98
 
99
+ function parseRowOrFail<T>(
100
+ schema: z.ZodType<T>,
101
+ value: unknown,
102
+ operation: string,
103
+ ): Effect.Effect<T, PlanExecutorInternalError> {
104
+ return Effect.try({
105
+ try: () => schema.parse(value),
106
+ catch: (cause) => new PlanExecutorInternalError({ message: `Failed to parse ${operation} row`, cause }),
107
+ })
108
+ }
109
+
100
110
  function withDatabaseTransactionEffect<A, E, R>(
101
111
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>,
102
112
  run: (tx: DatabaseTransaction) => Effect.Effect<A, E, R>,
@@ -310,7 +320,7 @@ function persistNodeResultAttemptEffect(
310
320
  issues: [...submission.validation.blocking, ...submission.validation.warnings],
311
321
  })
312
322
 
313
- void (yield* fromPromise(() =>
323
+ const updatedAttemptRow = yield* fromPromise(() =>
314
324
  tx
315
325
  .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
316
326
  .content({
@@ -327,7 +337,8 @@ function persistNodeResultAttemptEffect(
327
337
  ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
328
338
  })
329
339
  .output('after'),
330
- ).pipe(Effect.map((row) => PlanNodeAttemptSchema.parse(row))))
340
+ )
341
+ void (yield* parseRowOrFail(PlanNodeAttemptSchema, updatedAttemptRow, 'plan node attempt'))
331
342
 
332
343
  const publishedArtifacts =
333
344
  submission.validation.blocking.length > 0
@@ -351,21 +362,20 @@ function persistNodeResultAttemptEffect(
351
362
  artifacts: publishedArtifacts,
352
363
  })
353
364
 
354
- const nextNodeRun = PlanNodeRunSchema.parse(
355
- yield* fromPromise(() =>
356
- tx
357
- .update(ensureRecordId(submission.nodeRun.id, TABLES.PLAN_NODE_RUN))
358
- .content(
359
- toNodeRunData(submission.nodeRun, {
360
- attemptCount: submission.nodeRun.attemptCount + 1,
361
- latestAttemptId: attempt.id,
362
- latestStructuredOutput: result.structuredOutput ?? null,
363
- latestNotes: result.notes,
364
- }),
365
- )
366
- .output('after'),
367
- ),
365
+ const nextNodeRunRow = yield* fromPromise(() =>
366
+ tx
367
+ .update(ensureRecordId(submission.nodeRun.id, TABLES.PLAN_NODE_RUN))
368
+ .content(
369
+ toNodeRunData(submission.nodeRun, {
370
+ attemptCount: submission.nodeRun.attemptCount + 1,
371
+ latestAttemptId: attempt.id,
372
+ latestStructuredOutput: result.structuredOutput ?? null,
373
+ latestNotes: result.notes,
374
+ }),
375
+ )
376
+ .output('after'),
368
377
  )
378
+ const nextNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, nextNodeRunRow, 'plan node run')
369
379
 
370
380
  const nodeRuns = yield* context.planRunService.listNodeRuns(submission.run.id)
371
381
 
@@ -391,24 +401,23 @@ function handleRetryNodeResultEffect(
391
401
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
392
402
 
393
403
  return Effect.gen(function* () {
394
- const retryNodeRun = PlanNodeRunSchema.parse(
395
- yield* fromPromise(() =>
396
- tx
397
- .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
398
- .content(
399
- toNodeRunData(persisted.nextNodeRun, {
400
- status: 'ready',
401
- retryCount: persisted.nextNodeRun.retryCount + 1,
402
- failureClass: submission.validation.failureClass,
403
- blockedReason: submission.validation.blocking[0]?.message ?? null,
404
- readyAt: nowDate(),
405
- startedAt: null,
406
- completedAt: null,
407
- }),
408
- )
409
- .output('after'),
410
- ),
404
+ const retryNodeRunRow = yield* fromPromise(() =>
405
+ tx
406
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
407
+ .content(
408
+ toNodeRunData(persisted.nextNodeRun, {
409
+ status: 'ready',
410
+ retryCount: persisted.nextNodeRun.retryCount + 1,
411
+ failureClass: submission.validation.failureClass,
412
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
413
+ readyAt: nowDate(),
414
+ startedAt: null,
415
+ completedAt: null,
416
+ }),
417
+ )
418
+ .output('after'),
411
419
  )
420
+ const retryNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, retryNodeRunRow, 'plan node run')
412
421
 
413
422
  yield* emitEvent({
414
423
  tx,
@@ -477,22 +486,21 @@ function handleHumanReviewNodeResultEffect(
477
486
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
478
487
 
479
488
  return Effect.gen(function* () {
480
- const awaitingHumanNodeRun = PlanNodeRunSchema.parse(
481
- yield* fromPromise(() =>
482
- tx
483
- .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
484
- .content(
485
- toNodeRunData(persisted.nextNodeRun, {
486
- status: 'awaiting-human',
487
- retryCount: persisted.nextNodeRun.retryCount + 1,
488
- failureClass: submission.validation.failureClass,
489
- blockedReason: submission.validation.blocking[0]?.message ?? null,
490
- startedAt: persisted.nextNodeRun.startedAt ?? nowDate(),
491
- }),
492
- )
493
- .output('after'),
494
- ),
489
+ const awaitingHumanNodeRunRow = yield* fromPromise(() =>
490
+ tx
491
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
492
+ .content(
493
+ toNodeRunData(persisted.nextNodeRun, {
494
+ status: 'awaiting-human',
495
+ retryCount: persisted.nextNodeRun.retryCount + 1,
496
+ failureClass: submission.validation.failureClass,
497
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
498
+ startedAt: persisted.nextNodeRun.startedAt ?? nowDate(),
499
+ }),
500
+ )
501
+ .output('after'),
495
502
  )
503
+ const awaitingHumanNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, awaitingHumanNodeRunRow, 'plan node run')
496
504
 
497
505
  const approval = yield* context.planApprovalService
498
506
  .createPendingApproval({
@@ -561,21 +569,20 @@ function handleReplanNodeResultEffect(
561
569
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
562
570
 
563
571
  return Effect.gen(function* () {
564
- const blockedNodeRun = PlanNodeRunSchema.parse(
565
- yield* fromPromise(() =>
566
- tx
567
- .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
568
- .content(
569
- toNodeRunData(persisted.nextNodeRun, {
570
- status: 'blocked',
571
- retryCount: persisted.nextNodeRun.retryCount + 1,
572
- failureClass: submission.validation.failureClass,
573
- blockedReason: submission.validation.blocking[0]?.message ?? null,
574
- }),
575
- )
576
- .output('after'),
577
- ),
572
+ const blockedNodeRunRow = yield* fromPromise(() =>
573
+ tx
574
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
575
+ .content(
576
+ toNodeRunData(persisted.nextNodeRun, {
577
+ status: 'blocked',
578
+ retryCount: persisted.nextNodeRun.retryCount + 1,
579
+ failureClass: submission.validation.failureClass,
580
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
581
+ }),
582
+ )
583
+ .output('after'),
578
584
  )
585
+ const blockedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, blockedNodeRunRow, 'plan node run')
579
586
 
580
587
  const blockedRun = yield* replaceRun(tx, submission.run, {
581
588
  status: 'blocked',
@@ -629,22 +636,21 @@ function handleFailedNodeResultEffect(
629
636
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
630
637
 
631
638
  return Effect.gen(function* () {
632
- const failedNodeRun = PlanNodeRunSchema.parse(
633
- yield* fromPromise(() =>
634
- tx
635
- .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
636
- .content(
637
- toNodeRunData(persisted.nextNodeRun, {
638
- status: 'failed',
639
- retryCount: persisted.nextNodeRun.retryCount + 1,
640
- failureClass: submission.validation.failureClass,
641
- blockedReason: submission.validation.blocking[0]?.message ?? null,
642
- completedAt: nowDate(),
643
- }),
644
- )
645
- .output('after'),
646
- ),
639
+ const failedNodeRunRow = yield* fromPromise(() =>
640
+ tx
641
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
642
+ .content(
643
+ toNodeRunData(persisted.nextNodeRun, {
644
+ status: 'failed',
645
+ retryCount: persisted.nextNodeRun.retryCount + 1,
646
+ failureClass: submission.validation.failureClass,
647
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
648
+ completedAt: nowDate(),
649
+ }),
650
+ )
651
+ .output('after'),
647
652
  )
653
+ const failedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, failedNodeRunRow, 'plan node run')
648
654
 
649
655
  const failedRun = yield* replaceRun(tx, submission.run, {
650
656
  status: 'failed',
@@ -735,24 +741,23 @@ function handleSuccessfulNodeResultEffect(
735
741
  const { tx, emittedEvents, submission, persisted, emittedBy, result } = params
736
742
 
737
743
  return Effect.gen(function* () {
738
- const completedNodeRun = PlanNodeRunSchema.parse(
739
- yield* fromPromise(() =>
740
- tx
741
- .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
742
- .content(
743
- toNodeRunData(persisted.nextNodeRun, {
744
- status: submission.validation.warnings.length > 0 ? 'partial' : 'completed',
745
- latestAttemptId: persisted.attemptId,
746
- latestStructuredOutput: result.structuredOutput ?? null,
747
- latestNotes: result.notes,
748
- blockedReason: null,
749
- failureClass: null,
750
- completedAt: nowDate(),
751
- }),
752
- )
753
- .output('after'),
754
- ),
744
+ const completedNodeRunRow = yield* fromPromise(() =>
745
+ tx
746
+ .update(ensureRecordId(persisted.nextNodeRun.id, TABLES.PLAN_NODE_RUN))
747
+ .content(
748
+ toNodeRunData(persisted.nextNodeRun, {
749
+ status: submission.validation.warnings.length > 0 ? 'partial' : 'completed',
750
+ latestAttemptId: persisted.attemptId,
751
+ latestStructuredOutput: result.structuredOutput ?? null,
752
+ latestNotes: result.notes,
753
+ blockedReason: null,
754
+ failureClass: null,
755
+ completedAt: nowDate(),
756
+ }),
757
+ )
758
+ .output('after'),
755
759
  )
760
+ const completedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, completedNodeRunRow, 'plan node run')
756
761
 
757
762
  yield* emitEvent({
758
763
  tx,
@@ -949,7 +954,7 @@ function persistHumanNodeResponseAttemptEffect(
949
954
  issues: [...submission.validation.blocking, ...submission.validation.warnings],
950
955
  })
951
956
 
952
- void (yield* fromPromise(() =>
957
+ const updatedHumanAttemptRow = yield* fromPromise(() =>
953
958
  tx
954
959
  .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
955
960
  .content({
@@ -966,27 +971,27 @@ function persistHumanNodeResponseAttemptEffect(
966
971
  ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
967
972
  })
968
973
  .output('after'),
969
- ).pipe(Effect.map((row) => PlanNodeAttemptSchema.parse(row))))
974
+ )
975
+ void (yield* parseRowOrFail(PlanNodeAttemptSchema, updatedHumanAttemptRow, 'plan node attempt'))
970
976
 
971
- const nextNodeRun = PlanNodeRunSchema.parse(
972
- yield* fromPromise(() =>
973
- tx
974
- .update(ensureRecordId(submission.nodeRun.id, TABLES.PLAN_NODE_RUN))
975
- .content(
976
- toNodeRunData(submission.nodeRun, {
977
- status: submission.validation.blocking.length > 0 ? 'blocked' : 'completed',
978
- attemptCount: submission.nodeRun.attemptCount + 1,
979
- latestAttemptId: attempt.id,
980
- latestStructuredOutput: response,
981
- latestNotes: submission.responseComments ?? null,
982
- blockedReason: submission.validation.blocking[0]?.message ?? null,
983
- failureClass: submission.validation.blocking.length > 0 ? submission.validation.failureClass : null,
984
- ...(submission.validation.blocking.length > 0 ? {} : { completedAt: nowDate() }),
985
- }),
986
- )
987
- .output('after'),
988
- ),
977
+ const nextNodeRunRow = yield* fromPromise(() =>
978
+ tx
979
+ .update(ensureRecordId(submission.nodeRun.id, TABLES.PLAN_NODE_RUN))
980
+ .content(
981
+ toNodeRunData(submission.nodeRun, {
982
+ status: submission.validation.blocking.length > 0 ? 'blocked' : 'completed',
983
+ attemptCount: submission.nodeRun.attemptCount + 1,
984
+ latestAttemptId: attempt.id,
985
+ latestStructuredOutput: response,
986
+ latestNotes: submission.responseComments ?? null,
987
+ blockedReason: submission.validation.blocking[0]?.message ?? null,
988
+ failureClass: submission.validation.blocking.length > 0 ? submission.validation.failureClass : null,
989
+ ...(submission.validation.blocking.length > 0 ? {} : { completedAt: nowDate() }),
990
+ }),
991
+ )
992
+ .output('after'),
989
993
  )
994
+ const nextNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, nextNodeRunRow, 'plan node run')
990
995
 
991
996
  const nodeRuns = replaceNodeRunInList(yield* context.planRunService.listNodeRuns(submission.run.id), nextNodeRun)
992
997
 
@@ -1105,8 +1110,7 @@ function handleAcceptedHumanNodeResponseEffect(
1105
1110
  })
1106
1111
  }
1107
1112
 
1108
- /** @lintignore */
1109
- function submitNodeResult(
1113
+ const submitNodeResult = Effect.fn('PlanExecutor.submitNodeResult')(function* (
1110
1114
  context: PlanExecutorContext,
1111
1115
  params: {
1112
1116
  threadId: RecordIdInput
@@ -1115,7 +1119,7 @@ function submitNodeResult(
1115
1119
  emittedBy: string
1116
1120
  result: PlanNodeResultSubmission
1117
1121
  },
1118
- ): Promise<ExecutionPlanToolResultData> {
1122
+ ) {
1119
1123
  const {
1120
1124
  databaseService,
1121
1125
  generatedDocumentStorageService,
@@ -1124,55 +1128,58 @@ function submitNodeResult(
1124
1128
  planRunService,
1125
1129
  } = context
1126
1130
 
1127
- return Effect.gen(function* () {
1128
- const submission = yield* loadNodeResultSubmissionContextEffect(context, params)
1129
- const publishedArtifactStorageKeys: string[] = []
1130
-
1131
- yield* withTransactionAndEventsEffect({
1132
- db: databaseService,
1133
- planEventDeliveryService,
1134
- run: (tx, emittedEvents) =>
1135
- Effect.gen(function* () {
1136
- const persisted = yield* persistNodeResultAttemptEffect(context, {
1137
- tx,
1138
- submission,
1139
- nodeId: params.nodeId,
1140
- emittedBy: params.emittedBy,
1141
- result: params.result,
1142
- publishedArtifactStorageKeys,
1143
- })
1131
+ const submission = yield* loadNodeResultSubmissionContextEffect(context, params)
1132
+ const publishedArtifactStorageKeys: string[] = []
1133
+
1134
+ yield* withTransactionAndEventsEffect({
1135
+ db: databaseService,
1136
+ planEventDeliveryService,
1137
+ run: (tx, emittedEvents) =>
1138
+ Effect.gen(function* () {
1139
+ const persisted = yield* persistNodeResultAttemptEffect(context, {
1140
+ tx,
1141
+ submission,
1142
+ nodeId: params.nodeId,
1143
+ emittedBy: params.emittedBy,
1144
+ result: params.result,
1145
+ publishedArtifactStorageKeys,
1146
+ })
1144
1147
 
1145
- if (submission.validation.blocking.length > 0) {
1146
- return yield* handleBlockedNodeResultEffect(context, {
1147
- tx,
1148
- emittedEvents,
1149
- submission,
1150
- persisted,
1151
- emittedBy: params.emittedBy,
1152
- })
1153
- }
1154
-
1155
- return yield* handleSuccessfulNodeResultEffect(context, {
1148
+ if (submission.validation.blocking.length > 0) {
1149
+ return yield* handleBlockedNodeResultEffect(context, {
1156
1150
  tx,
1157
1151
  emittedEvents,
1158
1152
  submission,
1159
1153
  persisted,
1160
1154
  emittedBy: params.emittedBy,
1161
- result: params.result,
1162
1155
  })
1163
- }),
1164
- }).pipe(
1165
- Effect.tapError(() =>
1166
- Effect.forEach(publishedArtifactStorageKeys, (storageKey) =>
1167
- generatedDocumentStorageService.deleteTextArtifact(storageKey).pipe(Effect.ignore),
1168
- ).pipe(Effect.asVoid),
1169
- ),
1170
- )
1156
+ }
1171
1157
 
1172
- const orgId = recordIdToString(submission.run.organizationId, TABLES.ORGANIZATION)
1173
- const runIdStr = recordIdToString(submission.run.id, TABLES.PLAN_RUN)
1158
+ return yield* handleSuccessfulNodeResultEffect(context, {
1159
+ tx,
1160
+ emittedEvents,
1161
+ submission,
1162
+ persisted,
1163
+ emittedBy: params.emittedBy,
1164
+ result: params.result,
1165
+ })
1166
+ }),
1167
+ }).pipe(
1168
+ Effect.withSpan('PlanExecutor.submitNodeResult.persistTransaction'),
1169
+ Effect.tapError(() =>
1170
+ Effect.forEach(publishedArtifactStorageKeys, (storageKey) =>
1171
+ generatedDocumentStorageService.deleteTextArtifact(storageKey).pipe(Effect.ignore),
1172
+ ).pipe(Effect.asVoid),
1173
+ ),
1174
+ )
1174
1175
 
1175
- yield* fromPromise(() =>
1176
+ const orgId = recordIdToString(submission.run.organizationId, TABLES.ORGANIZATION)
1177
+ const runIdStr = recordIdToString(submission.run.id, TABLES.PLAN_RUN)
1178
+
1179
+ const background = yield* BackgroundWorkService
1180
+
1181
+ yield* background.run(
1182
+ fromPromise(() =>
1176
1183
  planCompletionSideEffects.runPlanNodeCompletionSideEffects({
1177
1184
  runId: runIdStr,
1178
1185
  organizationId: orgId,
@@ -1192,31 +1199,36 @@ function submitNodeResult(
1192
1199
  aiLogger.warn`Failed to record node completion metrics for run ${runIdStr} node ${params.nodeId}: ${error.message}`
1193
1200
  }),
1194
1201
  ),
1195
- Effect.forkDetach,
1196
- )
1202
+ ),
1203
+ 'plan-executor.recordNodeCompletionMetrics',
1204
+ )
1197
1205
 
1198
- const updatedRun = yield* planRunService.getRunById(submission.run.id)
1199
- if (updatedRun.status === 'completed') {
1200
- yield* fromPromise(() =>
1206
+ const updatedRun = yield* planRunService.getRunById(submission.run.id)
1207
+ if (updatedRun.status === 'completed') {
1208
+ yield* background.run(
1209
+ fromPromise(() =>
1201
1210
  planCompletionSideEffects.runPlanCompletionSideEffectsSafely({ runId: runIdStr, organizationId: orgId }),
1202
1211
  ).pipe(
1203
- Effect.catchTag('PlanExecutorInternalError', () => Effect.void),
1204
- Effect.forkDetach,
1205
- )
1206
- }
1212
+ Effect.catchTag('PlanExecutorInternalError', (error) =>
1213
+ Effect.sync(() => {
1214
+ aiLogger.warn`Plan completion side-effects failed for run ${runIdStr}: ${error.message}`
1215
+ }),
1216
+ ),
1217
+ ),
1218
+ 'plan-executor.runPlanCompletionSideEffects',
1219
+ )
1220
+ }
1207
1221
 
1208
- const snapshot = yield* serializeRunFull(planRunService, updatedRun)
1222
+ const snapshot = yield* serializeRunFull(planRunService, updatedRun)
1209
1223
 
1210
- return buildExecutionPlanToolResult({
1211
- action: 'node-result-submitted',
1212
- plan: snapshot,
1213
- message: `Submitted result for node "${submission.nodeSpec.label}".`,
1214
- })
1215
- }).pipe(runPromise)
1216
- }
1224
+ return buildExecutionPlanToolResult({
1225
+ action: 'node-result-submitted',
1226
+ plan: snapshot,
1227
+ message: `Submitted result for node "${submission.nodeSpec.label}".`,
1228
+ })
1229
+ })
1217
1230
 
1218
- /** @lintignore */
1219
- function submitHumanNodeResponse(
1231
+ const submitHumanNodeResponse = Effect.fn('PlanExecutor.submitHumanNodeResponse')(function* (
1220
1232
  context: PlanExecutorContext,
1221
1233
  params: {
1222
1234
  threadId: RecordIdInput
@@ -1225,195 +1237,186 @@ function submitHumanNodeResponse(
1225
1237
  response: HumanNodeResponsePayload
1226
1238
  approvalMessageId?: string
1227
1239
  },
1228
- ): Promise<SerializableExecutionPlan | null> {
1240
+ ) {
1229
1241
  const { databaseService, planEventDeliveryService } = context
1230
1242
 
1231
- return Effect.gen(function* () {
1232
- const submission = yield* loadHumanNodeResponseContextEffect(context, params)
1233
- if (!submission) {
1234
- return null
1235
- }
1243
+ const submission = yield* loadHumanNodeResponseContextEffect(context, params)
1244
+ if (!submission) {
1245
+ return null
1246
+ }
1236
1247
 
1237
- return yield* withTransactionAndEventsEffect({
1238
- db: databaseService,
1239
- planEventDeliveryService,
1240
- run: (tx, emittedEvents) =>
1241
- Effect.gen(function* () {
1242
- const persisted = yield* persistHumanNodeResponseAttemptEffect(context, {
1243
- tx,
1244
- submission,
1245
- respondedBy: params.respondedBy,
1246
- approvalMessageId: params.approvalMessageId,
1247
- response: params.response,
1248
- })
1248
+ return yield* withTransactionAndEventsEffect({
1249
+ db: databaseService,
1250
+ planEventDeliveryService,
1251
+ run: (tx, emittedEvents) =>
1252
+ Effect.gen(function* () {
1253
+ const persisted = yield* persistHumanNodeResponseAttemptEffect(context, {
1254
+ tx,
1255
+ submission,
1256
+ respondedBy: params.respondedBy,
1257
+ approvalMessageId: params.approvalMessageId,
1258
+ response: params.response,
1259
+ })
1249
1260
 
1250
- if (submission.validation.blocking.length > 0) {
1251
- return yield* handleBlockedHumanNodeResponseEffect(context, {
1252
- tx,
1253
- emittedEvents,
1254
- submission,
1255
- persisted,
1256
- respondedBy: params.respondedBy,
1257
- })
1258
- }
1259
-
1260
- return yield* handleAcceptedHumanNodeResponseEffect(context, {
1261
+ if (submission.validation.blocking.length > 0) {
1262
+ return yield* handleBlockedHumanNodeResponseEffect(context, {
1261
1263
  tx,
1262
1264
  emittedEvents,
1263
1265
  submission,
1264
1266
  persisted,
1265
1267
  respondedBy: params.respondedBy,
1266
1268
  })
1267
- }),
1268
- })
1269
- }).pipe(runPromise)
1270
- }
1269
+ }
1271
1270
 
1272
- /** @lintignore */
1273
- function resumeRun(
1271
+ return yield* handleAcceptedHumanNodeResponseEffect(context, {
1272
+ tx,
1273
+ emittedEvents,
1274
+ submission,
1275
+ persisted,
1276
+ respondedBy: params.respondedBy,
1277
+ })
1278
+ }),
1279
+ }).pipe(Effect.withSpan('PlanExecutor.submitHumanNodeResponse.persistTransaction'))
1280
+ })
1281
+
1282
+ const resumeRun = Effect.fn('PlanExecutor.resumeRun')(function* (
1274
1283
  context: PlanExecutorContext,
1275
1284
  params: { threadId: RecordIdInput; runId: string; emittedBy: string },
1276
- ): Promise<ExecutionPlanToolResultData> {
1285
+ ) {
1277
1286
  const { databaseService, planEventDeliveryService, planRunService } = context
1278
1287
 
1279
- return Effect.gen(function* () {
1280
- const run = yield* planRunService.getRunById(params.runId)
1281
- const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1282
- const nodeSpecs = yield* planRunService.listNodeSpecs(spec.id)
1283
- const nodeRuns = yield* planRunService.listNodeRuns(run.id)
1284
- const artifacts = yield* planRunService.listArtifacts(run.id)
1285
- const latestCheckpoint = yield* planRunService.getLatestCheckpoint(run.id)
1286
- const nextCheckpointSequence = yield* planRunService.getNextCheckpointSequence(run.id)
1287
-
1288
- const snapshot = yield* withTransactionAndEventsEffect({
1289
- db: databaseService,
1290
- planEventDeliveryService,
1291
- run: (tx, emittedEvents) =>
1292
- Effect.gen(function* () {
1293
- let currentNodeRuns = [...nodeRuns]
1294
- for (const currentNodeRun of currentNodeRuns.filter((candidate) => candidate.status === 'running')) {
1295
- const resetNodeRun = PlanNodeRunSchema.parse(
1296
- yield* fromPromise(() =>
1297
- tx
1298
- .update(ensureRecordId(currentNodeRun.id, TABLES.PLAN_NODE_RUN))
1299
- .content(
1300
- toNodeRunData(currentNodeRun, {
1301
- status: 'ready',
1302
- readyAt: nowDate(),
1303
- startedAt: currentNodeRun.startedAt ?? nowDate(),
1304
- }),
1305
- )
1306
- .output('after'),
1307
- ),
1308
- )
1309
- currentNodeRuns = currentNodeRuns.map((candidate) =>
1310
- candidate.nodeId === resetNodeRun.nodeId ? resetNodeRun : candidate,
1311
- )
1312
- }
1313
-
1314
- const resetRun = yield* replaceRun(tx, run, {
1315
- status: run.status === 'awaiting-human' ? 'awaiting-human' : 'running',
1316
- currentNodeId: run.status === 'awaiting-human' ? (run.currentNodeId ?? null) : null,
1317
- waitingNodeId: run.status === 'awaiting-human' ? (run.waitingNodeId ?? null) : null,
1318
- readyNodeIds: currentNodeRuns
1319
- .filter((candidate) => candidate.status === 'ready')
1320
- .map((candidate) => candidate.nodeId),
1321
- })
1288
+ const run = yield* planRunService.getRunById(params.runId)
1289
+ const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1290
+ const [nodeSpecs, nodeRuns, artifacts, latestCheckpoint, nextCheckpointSequence] = yield* Effect.all([
1291
+ planRunService.listNodeSpecs(spec.id),
1292
+ planRunService.listNodeRuns(run.id),
1293
+ planRunService.listArtifacts(run.id),
1294
+ planRunService.getLatestCheckpoint(run.id),
1295
+ planRunService.getNextCheckpointSequence(run.id),
1296
+ ]).pipe(Effect.withSpan('PlanExecutor.resumeRun.loadRunGraph'))
1297
+
1298
+ const snapshot = yield* withTransactionAndEventsEffect({
1299
+ db: databaseService,
1300
+ planEventDeliveryService,
1301
+ run: (tx, emittedEvents) =>
1302
+ Effect.gen(function* () {
1303
+ let currentNodeRuns = [...nodeRuns]
1304
+ for (const currentNodeRun of currentNodeRuns.filter((candidate) => candidate.status === 'running')) {
1305
+ const resetNodeRunRow = yield* fromPromise(() =>
1306
+ tx
1307
+ .update(ensureRecordId(currentNodeRun.id, TABLES.PLAN_NODE_RUN))
1308
+ .content(
1309
+ toNodeRunData(currentNodeRun, {
1310
+ status: 'ready',
1311
+ readyAt: nowDate(),
1312
+ startedAt: currentNodeRun.startedAt ?? nowDate(),
1313
+ }),
1314
+ )
1315
+ .output('after'),
1316
+ )
1317
+ const resetNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, resetNodeRunRow, 'plan node run')
1318
+ currentNodeRuns = currentNodeRuns.map((candidate) =>
1319
+ candidate.nodeId === resetNodeRun.nodeId ? resetNodeRun : candidate,
1320
+ )
1321
+ }
1322
+
1323
+ const resetRun = yield* replaceRun(tx, run, {
1324
+ status: run.status === 'awaiting-human' ? 'awaiting-human' : 'running',
1325
+ currentNodeId: run.status === 'awaiting-human' ? (run.currentNodeId ?? null) : null,
1326
+ waitingNodeId: run.status === 'awaiting-human' ? (run.waitingNodeId ?? null) : null,
1327
+ readyNodeIds: currentNodeRuns
1328
+ .filter((candidate) => candidate.status === 'ready')
1329
+ .map((candidate) => candidate.nodeId),
1330
+ })
1322
1331
 
1323
- yield* emitEvent({
1324
- tx,
1325
- run: resetRun,
1326
- spec,
1327
- eventType: 'run-resumed',
1328
- fromStatus: run.status,
1329
- toStatus: resetRun.status,
1330
- message: `Run "${spec.title}" resumed from the latest checkpoint.`,
1331
- detail: latestCheckpoint
1332
- ? { checkpointId: recordIdToString(latestCheckpoint.id, TABLES.PLAN_CHECKPOINT) }
1333
- : {},
1334
- emittedBy: params.emittedBy,
1335
- capturedEvents: emittedEvents,
1336
- })
1332
+ yield* emitEvent({
1333
+ tx,
1334
+ run: resetRun,
1335
+ spec,
1336
+ eventType: 'run-resumed',
1337
+ fromStatus: run.status,
1338
+ toStatus: resetRun.status,
1339
+ message: `Run "${spec.title}" resumed from the latest checkpoint.`,
1340
+ detail: latestCheckpoint
1341
+ ? { checkpointId: recordIdToString(latestCheckpoint.id, TABLES.PLAN_CHECKPOINT) }
1342
+ : {},
1343
+ emittedBy: params.emittedBy,
1344
+ capturedEvents: emittedEvents,
1345
+ })
1337
1346
 
1338
- const synced =
1339
- resetRun.status === 'awaiting-human'
1340
- ? { run: resetRun, nodeRuns: currentNodeRuns, artifacts }
1341
- : yield* fromPromise(() =>
1342
- syncRunGraph(context, {
1343
- tx,
1344
- run: resetRun,
1345
- spec,
1346
- nodeSpecs,
1347
- nodeRuns: currentNodeRuns,
1348
- artifacts,
1349
- emittedBy: params.emittedBy,
1350
- capturedEvents: emittedEvents,
1351
- }),
1352
- )
1353
-
1354
- const checkpoint = yield* saveCheckpointWithContext(context, {
1355
- tx,
1356
- run: synced.run,
1357
- spec,
1358
- nodeRuns: synced.nodeRuns,
1359
- artifacts: synced.artifacts,
1360
- sequence: nextCheckpointSequence,
1361
- reason: 'run-resumed',
1362
- capturedEvents: emittedEvents,
1363
- })
1364
- yield* attachCheckpoint(tx, synced.run, checkpoint)
1365
- const latestRun = yield* planRunService.getRunById(run.id)
1366
- return yield* serializeRunFull(planRunService, latestRun)
1367
- }),
1368
- })
1347
+ const synced =
1348
+ resetRun.status === 'awaiting-human'
1349
+ ? { run: resetRun, nodeRuns: currentNodeRuns, artifacts }
1350
+ : yield* fromPromise(() =>
1351
+ syncRunGraph(context, {
1352
+ tx,
1353
+ run: resetRun,
1354
+ spec,
1355
+ nodeSpecs,
1356
+ nodeRuns: currentNodeRuns,
1357
+ artifacts,
1358
+ emittedBy: params.emittedBy,
1359
+ capturedEvents: emittedEvents,
1360
+ }),
1361
+ )
1362
+
1363
+ const checkpoint = yield* saveCheckpointWithContext(context, {
1364
+ tx,
1365
+ run: synced.run,
1366
+ spec,
1367
+ nodeRuns: synced.nodeRuns,
1368
+ artifacts: synced.artifacts,
1369
+ sequence: nextCheckpointSequence,
1370
+ reason: 'run-resumed',
1371
+ capturedEvents: emittedEvents,
1372
+ })
1373
+ yield* attachCheckpoint(tx, synced.run, checkpoint)
1374
+ const latestRun = yield* planRunService.getRunById(run.id)
1375
+ return yield* serializeRunFull(planRunService, latestRun)
1376
+ }),
1377
+ }).pipe(Effect.withSpan('PlanExecutor.resumeRun.persistTransaction'))
1369
1378
 
1370
- return buildExecutionPlanToolResult({
1371
- action: 'run-resumed',
1372
- plan: snapshot,
1373
- message: `Resumed execution run "${snapshot.title}".`,
1374
- })
1375
- }).pipe(runPromise)
1376
- }
1379
+ return buildExecutionPlanToolResult({
1380
+ action: 'run-resumed',
1381
+ plan: snapshot,
1382
+ message: `Resumed execution run "${snapshot.title}".`,
1383
+ })
1384
+ })
1377
1385
 
1378
- /** @lintignore */
1379
- function transitionNodeToRunning(
1386
+ const transitionNodeToRunning = Effect.fn('PlanExecutor.transitionNodeToRunning')(function* (
1380
1387
  context: PlanExecutorContext,
1381
1388
  params: { runId: string; nodeId: string },
1382
- ): Promise<void> {
1389
+ ) {
1383
1390
  const { databaseService, planRunService } = context
1384
1391
 
1385
- return Effect.gen(function* () {
1386
- const run = yield* planRunService.getRunById(params.runId)
1387
- const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1388
- if (nodeRun.status !== 'ready') return
1392
+ const run = yield* planRunService.getRunById(params.runId)
1393
+ const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1394
+ if (nodeRun.status !== 'ready') return
1389
1395
 
1390
- yield* withDatabaseTransactionEffect(databaseService, (tx) =>
1391
- Effect.gen(function* () {
1392
- const runningNodeRun = PlanNodeRunSchema.parse(
1393
- yield* fromPromise(() =>
1394
- tx
1395
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1396
- .content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? nowDate() }))
1397
- .output('after'),
1398
- ),
1399
- )
1400
-
1401
- const nodeRuns = yield* planRunService.listNodeRuns(run.id)
1402
- yield* replaceRun(tx, run, {
1403
- status: 'running',
1404
- currentNodeId: runningNodeRun.nodeId,
1405
- waitingNodeId: null,
1406
- readyNodeIds: nodeRuns
1407
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
1408
- .map((candidate) => candidate.nodeId),
1409
- })
1410
- }),
1411
- )
1412
- }).pipe(runPromise)
1413
- }
1396
+ yield* withDatabaseTransactionEffect(databaseService, (tx) =>
1397
+ Effect.gen(function* () {
1398
+ const runningNodeRunRow = yield* fromPromise(() =>
1399
+ tx
1400
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1401
+ .content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? nowDate() }))
1402
+ .output('after'),
1403
+ )
1404
+ const runningNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, runningNodeRunRow, 'plan node run')
1405
+
1406
+ const nodeRuns = yield* planRunService.listNodeRuns(run.id)
1407
+ yield* replaceRun(tx, run, {
1408
+ status: 'running',
1409
+ currentNodeId: runningNodeRun.nodeId,
1410
+ waitingNodeId: null,
1411
+ readyNodeIds: nodeRuns
1412
+ .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
1413
+ .map((candidate) => candidate.nodeId),
1414
+ })
1415
+ }),
1416
+ ).pipe(Effect.withSpan('PlanExecutor.transitionNodeToRunning.persistTransaction'))
1417
+ })
1414
1418
 
1415
- /** @lintignore */
1416
- function blockNodeOnDispatchFailure(
1419
+ const blockNodeOnDispatchFailure = Effect.fn('PlanExecutor.blockNodeOnDispatchFailure')(function* (
1417
1420
  context: PlanExecutorContext,
1418
1421
  params: {
1419
1422
  threadId: RecordIdInput
@@ -1423,165 +1426,164 @@ function blockNodeOnDispatchFailure(
1423
1426
  message: string
1424
1427
  failureClass: PlanFailureClass
1425
1428
  },
1426
- ): Promise<SerializableExecutionPlan> {
1429
+ ) {
1427
1430
  const { databaseService, planEventDeliveryService, planRunService } = context
1428
1431
 
1429
- return Effect.gen(function* () {
1430
- const run = yield* planRunService.getRunById(params.runId)
1431
- const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1432
- const nodeSpec = yield* planRunService.getNodeSpecByNodeId(spec.id, params.nodeId)
1433
- const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1434
- const artifacts = yield* planRunService.listArtifacts(run.id)
1435
- const nextCheckpointSequence = yield* planRunService.getNextCheckpointSequence(run.id)
1436
-
1437
- return yield* withTransactionAndEventsEffect({
1438
- db: databaseService,
1439
- planEventDeliveryService,
1440
- run: (tx, emittedEvents) =>
1441
- Effect.gen(function* () {
1442
- const blockedNodeRun =
1443
- nodeRun.status === 'blocked'
1444
- ? nodeRun
1445
- : PlanNodeRunSchema.parse(
1446
- yield* fromPromise(() =>
1447
- tx
1448
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1449
- .content(
1450
- toNodeRunData(nodeRun, {
1451
- status: 'blocked',
1452
- blockedReason: params.message,
1453
- failureClass: params.failureClass,
1454
- }),
1455
- )
1456
- .output('after'),
1457
- ),
1458
- )
1459
-
1460
- const blockedRun = yield* replaceRun(tx, run, {
1461
- status: 'blocked',
1462
- currentNodeId: blockedNodeRun.nodeId,
1463
- waitingNodeId: null,
1464
- readyNodeIds: [],
1465
- failureCount: run.failureCount + 1,
1466
- })
1432
+ const run = yield* planRunService.getRunById(params.runId)
1433
+ const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1434
+ const [nodeSpec, nodeRun, artifacts, nextCheckpointSequence] = yield* Effect.all([
1435
+ planRunService.getNodeSpecByNodeId(spec.id, params.nodeId),
1436
+ planRunService.getNodeRunByNodeId(run.id, params.nodeId),
1437
+ planRunService.listArtifacts(run.id),
1438
+ planRunService.getNextCheckpointSequence(run.id),
1439
+ ]).pipe(Effect.withSpan('PlanExecutor.blockNodeOnDispatchFailure.loadNodeContext'))
1440
+
1441
+ return yield* withTransactionAndEventsEffect({
1442
+ db: databaseService,
1443
+ planEventDeliveryService,
1444
+ run: (tx, emittedEvents) =>
1445
+ Effect.gen(function* () {
1446
+ let blockedNodeRun: PlanNodeRunRecord
1447
+ if (nodeRun.status === 'blocked') {
1448
+ blockedNodeRun = nodeRun
1449
+ } else {
1450
+ const blockedNodeRunRow = yield* fromPromise(() =>
1451
+ tx
1452
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1453
+ .content(
1454
+ toNodeRunData(nodeRun, {
1455
+ status: 'blocked',
1456
+ blockedReason: params.message,
1457
+ failureClass: params.failureClass,
1458
+ }),
1459
+ )
1460
+ .output('after'),
1461
+ )
1462
+ blockedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, blockedNodeRunRow, 'plan node run')
1463
+ }
1467
1464
 
1468
- yield* emitEvent({
1469
- tx,
1470
- run: blockedRun,
1471
- spec,
1472
- nodeId: blockedNodeRun.nodeId,
1473
- eventType: 'node-blocked',
1474
- fromStatus: nodeRun.status,
1475
- toStatus: blockedNodeRun.status,
1476
- message: `Node "${nodeSpec.label}" failed during owner dispatch.`,
1477
- detail: { failureClass: params.failureClass, phase: 'dispatch', error: params.message },
1478
- emittedBy: params.emittedBy,
1479
- capturedEvents: emittedEvents,
1480
- })
1465
+ const blockedRun = yield* replaceRun(tx, run, {
1466
+ status: 'blocked',
1467
+ currentNodeId: blockedNodeRun.nodeId,
1468
+ waitingNodeId: null,
1469
+ readyNodeIds: [],
1470
+ failureCount: run.failureCount + 1,
1471
+ })
1481
1472
 
1482
- const checkpointNodeRuns = (yield* planRunService.listNodeRuns(run.id)).map((candidate) =>
1483
- candidate.nodeId === blockedNodeRun.nodeId ? blockedNodeRun : candidate,
1484
- )
1485
- const checkpoint = yield* saveCheckpointWithContext(context, {
1486
- tx,
1487
- run: blockedRun,
1488
- spec,
1489
- nodeRuns: checkpointNodeRuns,
1490
- artifacts,
1491
- sequence: nextCheckpointSequence,
1492
- reason: 'owner-dispatch-failed',
1493
- capturedEvents: emittedEvents,
1494
- })
1495
- yield* attachCheckpoint(tx, blockedRun, checkpoint)
1496
- const latestRun = yield* planRunService.getRunById(run.id)
1497
- return yield* serializeRunFull(planRunService, latestRun)
1498
- }),
1499
- })
1500
- }).pipe(runPromise)
1501
- }
1473
+ yield* emitEvent({
1474
+ tx,
1475
+ run: blockedRun,
1476
+ spec,
1477
+ nodeId: blockedNodeRun.nodeId,
1478
+ eventType: 'node-blocked',
1479
+ fromStatus: nodeRun.status,
1480
+ toStatus: blockedNodeRun.status,
1481
+ message: `Node "${nodeSpec.label}" failed during owner dispatch.`,
1482
+ detail: { failureClass: params.failureClass, phase: 'dispatch', error: params.message },
1483
+ emittedBy: params.emittedBy,
1484
+ capturedEvents: emittedEvents,
1485
+ })
1486
+
1487
+ const checkpointNodeRuns = (yield* planRunService.listNodeRuns(run.id)).map((candidate) =>
1488
+ candidate.nodeId === blockedNodeRun.nodeId ? blockedNodeRun : candidate,
1489
+ )
1490
+ const checkpoint = yield* saveCheckpointWithContext(context, {
1491
+ tx,
1492
+ run: blockedRun,
1493
+ spec,
1494
+ nodeRuns: checkpointNodeRuns,
1495
+ artifacts,
1496
+ sequence: nextCheckpointSequence,
1497
+ reason: 'owner-dispatch-failed',
1498
+ capturedEvents: emittedEvents,
1499
+ })
1500
+ yield* attachCheckpoint(tx, blockedRun, checkpoint)
1501
+ const latestRun = yield* planRunService.getRunById(run.id)
1502
+ return yield* serializeRunFull(planRunService, latestRun)
1503
+ }),
1504
+ }).pipe(Effect.withSpan('PlanExecutor.blockNodeOnDispatchFailure.persistTransaction'))
1505
+ })
1502
1506
 
1503
- /** @lintignore */
1504
- function promoteDelayedNode(
1507
+ const promoteDelayedNode = Effect.fn('PlanExecutor.promoteDelayedNode')(function* (
1505
1508
  context: PlanExecutorContext,
1506
1509
  params: { runId: string; nodeId: string; emittedBy: string },
1507
- ): Promise<void> {
1510
+ ) {
1508
1511
  const { databaseService, planEventDeliveryService, planRunService } = context
1509
1512
 
1510
- return Effect.gen(function* () {
1511
- const run = yield* planRunService.getRunById(params.runId)
1512
- if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
1513
- return
1514
- }
1513
+ const run = yield* planRunService.getRunById(params.runId)
1514
+ if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
1515
+ return
1516
+ }
1515
1517
 
1516
- const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1517
- const nodeSpecs = yield* planRunService.listNodeSpecs(spec.id)
1518
- const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1519
- if (nodeRun.status !== 'scheduled') return
1520
-
1521
- const nodeRuns = yield* planRunService.listNodeRuns(run.id)
1522
- const artifacts = yield* planRunService.listArtifacts(run.id)
1523
- const nextCheckpointSequence = yield* planRunService.getNextCheckpointSequence(run.id)
1524
-
1525
- yield* withTransactionAndEventsEffect({
1526
- db: databaseService,
1527
- planEventDeliveryService,
1528
- run: (tx, emittedEvents) =>
1529
- Effect.gen(function* () {
1530
- const readyNodeRun = PlanNodeRunSchema.parse(
1531
- yield* fromPromise(() =>
1532
- tx
1533
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1534
- .content(toNodeRunData(nodeRun, { status: 'ready', readyAt: nowDate() }))
1535
- .output('after'),
1536
- ),
1537
- )
1518
+ const spec = yield* planRunService.getPlanSpecById(run.planSpecId)
1519
+ const nodeSpecs = yield* planRunService.listNodeSpecs(spec.id)
1520
+ const nodeRun = yield* planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1521
+ if (nodeRun.status !== 'scheduled') return
1538
1522
 
1539
- const updatedNodeRuns = nodeRuns.map((candidate) =>
1540
- candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
1541
- )
1523
+ const [nodeRuns, artifacts, nextCheckpointSequence] = yield* Effect.all([
1524
+ planRunService.listNodeRuns(run.id),
1525
+ planRunService.listArtifacts(run.id),
1526
+ planRunService.getNextCheckpointSequence(run.id),
1527
+ ]).pipe(Effect.withSpan('PlanExecutor.promoteDelayedNode.loadRunContext'))
1528
+
1529
+ yield* withTransactionAndEventsEffect({
1530
+ db: databaseService,
1531
+ planEventDeliveryService,
1532
+ run: (tx, emittedEvents) =>
1533
+ Effect.gen(function* () {
1534
+ const readyNodeRunRow = yield* fromPromise(() =>
1535
+ tx
1536
+ .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1537
+ .content(toNodeRunData(nodeRun, { status: 'ready', readyAt: nowDate() }))
1538
+ .output('after'),
1539
+ )
1540
+ const readyNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, readyNodeRunRow, 'plan node run')
1542
1541
 
1543
- const currentNodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
1544
- yield* emitEvent({
1542
+ const updatedNodeRuns = nodeRuns.map((candidate) =>
1543
+ candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
1544
+ )
1545
+
1546
+ const currentNodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
1547
+ yield* emitEvent({
1548
+ tx,
1549
+ run,
1550
+ spec,
1551
+ nodeId: readyNodeRun.nodeId,
1552
+ eventType: 'node-ready',
1553
+ fromStatus: nodeRun.status,
1554
+ toStatus: readyNodeRun.status,
1555
+ message: `Node "${currentNodeSpec?.label ?? params.nodeId}" promoted to ready after delay.`,
1556
+ emittedBy: params.emittedBy,
1557
+ capturedEvents: emittedEvents,
1558
+ })
1559
+
1560
+ const synced = yield* fromPromise(() =>
1561
+ syncRunGraph(context, {
1545
1562
  tx,
1546
1563
  run,
1547
1564
  spec,
1548
- nodeId: readyNodeRun.nodeId,
1549
- eventType: 'node-ready',
1550
- fromStatus: nodeRun.status,
1551
- toStatus: readyNodeRun.status,
1552
- message: `Node "${currentNodeSpec?.label ?? params.nodeId}" promoted to ready after delay.`,
1565
+ nodeSpecs,
1566
+ nodeRuns: updatedNodeRuns,
1567
+ artifacts,
1553
1568
  emittedBy: params.emittedBy,
1554
1569
  capturedEvents: emittedEvents,
1555
- })
1556
-
1557
- const synced = yield* fromPromise(() =>
1558
- syncRunGraph(context, {
1559
- tx,
1560
- run,
1561
- spec,
1562
- nodeSpecs,
1563
- nodeRuns: updatedNodeRuns,
1564
- artifacts,
1565
- emittedBy: params.emittedBy,
1566
- capturedEvents: emittedEvents,
1567
- }),
1568
- )
1570
+ }),
1571
+ )
1569
1572
 
1570
- const checkpoint = yield* saveCheckpointWithContext(context, {
1571
- tx,
1572
- run: synced.run,
1573
- spec,
1574
- nodeRuns: synced.nodeRuns,
1575
- artifacts: synced.artifacts,
1576
- sequence: nextCheckpointSequence,
1577
- reason: 'delayed-node-promoted',
1578
- capturedEvents: emittedEvents,
1579
- })
1580
- yield* attachCheckpoint(tx, synced.run, checkpoint)
1581
- }),
1582
- })
1583
- }).pipe(runPromise)
1584
- }
1573
+ const checkpoint = yield* saveCheckpointWithContext(context, {
1574
+ tx,
1575
+ run: synced.run,
1576
+ spec,
1577
+ nodeRuns: synced.nodeRuns,
1578
+ artifacts: synced.artifacts,
1579
+ sequence: nextCheckpointSequence,
1580
+ reason: 'delayed-node-promoted',
1581
+ capturedEvents: emittedEvents,
1582
+ })
1583
+ yield* attachCheckpoint(tx, synced.run, checkpoint)
1584
+ }),
1585
+ }).pipe(Effect.withSpan('PlanExecutor.promoteDelayedNode.persistTransaction'))
1586
+ })
1585
1587
 
1586
1588
  export function makePlanExecutorService(deps: PlanExecutorDeps) {
1587
1589
  const context: PlanExecutorContext = {
@@ -1599,6 +1601,7 @@ export function makePlanExecutorService(deps: PlanExecutorDeps) {
1599
1601
  planSchedulerService: deps.planSchedulerService,
1600
1602
  planValidatorService: deps.planValidatorService,
1601
1603
  qualityMetricsService: deps.qualityMetricsService,
1604
+ delayedNodePromotionQueue: deps.delayedNodePromotionQueue,
1602
1605
  planCompletionSideEffects: makePlanCompletionSideEffects({
1603
1606
  databaseService: deps.db,
1604
1607
  feedbackLoopService: deps.feedbackLoopService,
@@ -1626,7 +1629,7 @@ export function makePlanExecutorService(deps: PlanExecutorDeps) {
1626
1629
  }
1627
1630
 
1628
1631
  export class PlanExecutorServiceTag extends Context.Service<PlanExecutorServiceTag, PlanExecutorService>()(
1629
- 'PlanExecutorService',
1632
+ '@lota-sdk/core/PlanExecutorService',
1630
1633
  ) {}
1631
1634
 
1632
1635
  export const PlanExecutorServiceLive = Layer.effect(
@@ -1634,6 +1637,7 @@ export const PlanExecutorServiceLive = Layer.effect(
1634
1637
  Effect.gen(function* () {
1635
1638
  const db = yield* DatabaseServiceTag
1636
1639
  const storage = yield* GeneratedDocumentStorageServiceTag
1640
+ const queues = yield* LotaQueuesServiceTag
1637
1641
  return makePlanExecutorService({
1638
1642
  db,
1639
1643
  storage,
@@ -1649,6 +1653,7 @@ export const PlanExecutorServiceLive = Layer.effect(
1649
1653
  planSchedulerService: yield* PlanSchedulerServiceTag,
1650
1654
  planValidatorService: yield* PlanValidatorServiceTag,
1651
1655
  qualityMetricsService: yield* QualityMetricsServiceTag,
1656
+ delayedNodePromotionQueue: queues.delayedNodePromotion,
1652
1657
  })
1653
1658
  }),
1654
1659
  )