@lota-sdk/core 0.4.9 → 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 (158) 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 +38 -10
  4. package/src/config/agent-defaults.ts +22 -9
  5. package/src/config/agent-types.ts +1 -1
  6. package/src/config/background-processing.ts +1 -1
  7. package/src/config/index.ts +0 -1
  8. package/src/config/logger.ts +20 -7
  9. package/src/config/thread-defaults.ts +12 -4
  10. package/src/create-runtime.ts +69 -656
  11. package/src/db/memory-query-builder.ts +2 -1
  12. package/src/db/memory-store.ts +29 -20
  13. package/src/db/memory.ts +188 -195
  14. package/src/db/service-normalization.ts +97 -64
  15. package/src/db/service.ts +706 -538
  16. package/src/db/startup.ts +30 -19
  17. package/src/effect/awaitable-effect.ts +46 -37
  18. package/src/effect/helpers.ts +30 -5
  19. package/src/effect/index.ts +7 -5
  20. package/src/effect/layers.ts +82 -72
  21. package/src/effect/runtime.ts +18 -3
  22. package/src/effect/services.ts +15 -11
  23. package/src/embeddings/provider.ts +65 -66
  24. package/src/index.ts +13 -11
  25. package/src/queues/autonomous-job.queue.ts +59 -71
  26. package/src/queues/context-compaction.queue.ts +6 -18
  27. package/src/queues/delayed-node-promotion.queue.ts +9 -17
  28. package/src/queues/organization-learning.queue.ts +17 -4
  29. package/src/queues/plan-agent-heartbeat.queue.ts +23 -20
  30. package/src/queues/plan-scheduler.queue.ts +6 -18
  31. package/src/queues/post-chat-memory.queue.ts +6 -18
  32. package/src/queues/queue-factory.ts +128 -50
  33. package/src/queues/title-generation.queue.ts +6 -17
  34. package/src/redis/connection.ts +181 -164
  35. package/src/redis/runtime-connection.ts +13 -3
  36. package/src/redis/stream-context.ts +17 -9
  37. package/src/runtime/agent-runtime-policy.ts +1 -1
  38. package/src/runtime/agent-stream-helpers.ts +15 -11
  39. package/src/runtime/chat-run-orchestration.ts +1 -1
  40. package/src/runtime/context-compaction/context-compaction-runtime.ts +1 -1
  41. package/src/runtime/context-compaction/context-compaction.ts +126 -82
  42. package/src/runtime/domain-layer.ts +192 -0
  43. package/src/runtime/graph-designer.ts +15 -7
  44. package/src/runtime/helper-model.ts +8 -4
  45. package/src/runtime/index.ts +0 -1
  46. package/src/runtime/memory/memory-block.ts +19 -9
  47. package/src/runtime/memory/memory-pipeline.ts +53 -66
  48. package/src/runtime/memory/memory-scope.ts +33 -29
  49. package/src/runtime/plugin-resolution.ts +33 -54
  50. package/src/runtime/post-turn-side-effects.ts +6 -26
  51. package/src/runtime/retrieval-adapters.ts +4 -4
  52. package/src/runtime/runtime-accessors.ts +92 -0
  53. package/src/runtime/runtime-config.ts +3 -3
  54. package/src/runtime/runtime-extensions.ts +20 -9
  55. package/src/runtime/runtime-lifecycle.ts +124 -0
  56. package/src/runtime/runtime-services.ts +386 -0
  57. package/src/runtime/runtime-token.ts +47 -0
  58. package/src/runtime/social-chat/social-chat-agent-runner.ts +7 -5
  59. package/src/runtime/social-chat/social-chat-history.ts +21 -12
  60. package/src/runtime/social-chat/social-chat.ts +401 -365
  61. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +58 -52
  62. package/src/runtime/thread-turn-context.ts +21 -27
  63. package/src/services/agent-activity.service.ts +1 -1
  64. package/src/services/agent-executor.service.ts +179 -187
  65. package/src/services/artifact.service.ts +10 -5
  66. package/src/services/attachment.service.ts +35 -1
  67. package/src/services/autonomous-job.service.ts +58 -56
  68. package/src/services/background-work.service.ts +54 -0
  69. package/src/services/chat-run-registry.service.ts +3 -1
  70. package/src/services/context-compaction.service.ts +1 -1
  71. package/src/services/document-chunk.service.ts +8 -17
  72. package/src/services/execution-plan/execution-plan-graph.ts +74 -52
  73. package/src/services/execution-plan/execution-plan.service.ts +1 -1
  74. package/src/services/feedback-loop.service.ts +1 -1
  75. package/src/services/global-orchestrator.service.ts +33 -10
  76. package/src/services/graph-full-routing.ts +44 -33
  77. package/src/services/index.ts +1 -0
  78. package/src/services/institutional-memory.service.ts +8 -17
  79. package/src/services/learned-skill.service.ts +38 -35
  80. package/src/services/memory/memory-errors.ts +27 -0
  81. package/src/services/memory/memory-org-memory.ts +14 -3
  82. package/src/services/memory/memory-preseeded.ts +10 -4
  83. package/src/services/memory/memory-utils.ts +2 -1
  84. package/src/services/memory/memory.service.ts +26 -44
  85. package/src/services/memory/rerank.service.ts +3 -11
  86. package/src/services/monitoring-window.service.ts +1 -1
  87. package/src/services/mutating-approval.service.ts +1 -1
  88. package/src/services/node-workspace.service.ts +2 -2
  89. package/src/services/notification.service.ts +16 -4
  90. package/src/services/organization-member.service.ts +1 -1
  91. package/src/services/organization.service.ts +34 -51
  92. package/src/services/ownership-dispatcher.service.ts +132 -90
  93. package/src/services/plan/plan-agent-heartbeat.service.ts +1 -1
  94. package/src/services/plan/plan-agent-query.service.ts +1 -1
  95. package/src/services/plan/plan-approval.service.ts +52 -48
  96. package/src/services/plan/plan-artifact.service.ts +2 -2
  97. package/src/services/plan/plan-builder.service.ts +2 -2
  98. package/src/services/plan/plan-checkpoint.service.ts +1 -1
  99. package/src/services/plan/plan-compiler.service.ts +1 -1
  100. package/src/services/plan/plan-completion-side-effects.ts +18 -24
  101. package/src/services/plan/plan-coordination.service.ts +1 -1
  102. package/src/services/plan/plan-cycle.service.ts +171 -164
  103. package/src/services/plan/plan-deadline.service.ts +290 -304
  104. package/src/services/plan/plan-event-delivery.service.ts +44 -39
  105. package/src/services/plan/plan-executor-graph.ts +114 -67
  106. package/src/services/plan/plan-executor-helpers.ts +60 -75
  107. package/src/services/plan/plan-executor.service.ts +550 -467
  108. package/src/services/plan/plan-run.service.ts +12 -19
  109. package/src/services/plan/plan-scheduler.service.ts +27 -33
  110. package/src/services/plan/plan-template.service.ts +1 -1
  111. package/src/services/plan/plan-transaction-events.ts +8 -5
  112. package/src/services/plan/plan-validator.service.ts +1 -1
  113. package/src/services/plan/plan-workspace.service.ts +17 -11
  114. package/src/services/plugin-executor.service.ts +26 -21
  115. package/src/services/quality-metrics.service.ts +1 -1
  116. package/src/services/queue-job.service.ts +8 -17
  117. package/src/services/recent-activity-title.service.ts +17 -9
  118. package/src/services/recent-activity.service.ts +1 -1
  119. package/src/services/skill-resolver.service.ts +1 -1
  120. package/src/services/social-chat-history.service.ts +37 -20
  121. package/src/services/system-executor.service.ts +25 -20
  122. package/src/services/thread/thread-bootstrap.ts +26 -10
  123. package/src/services/thread/thread-listing.ts +2 -1
  124. package/src/services/thread/thread-memory-block.ts +18 -5
  125. package/src/services/thread/thread-message.service.ts +24 -8
  126. package/src/services/thread/thread-title.service.ts +1 -1
  127. package/src/services/thread/thread-turn-execution.ts +1 -1
  128. package/src/services/thread/thread-turn-preparation.service.ts +18 -16
  129. package/src/services/thread/thread-turn-streaming.ts +12 -11
  130. package/src/services/thread/thread-turn.ts +43 -10
  131. package/src/services/thread/thread.service.ts +11 -2
  132. package/src/services/user.service.ts +1 -1
  133. package/src/services/write-intent-validator.service.ts +1 -1
  134. package/src/storage/attachment-storage.service.ts +7 -4
  135. package/src/storage/generated-document-storage.service.ts +1 -1
  136. package/src/system-agents/context-compaction.agent.ts +1 -1
  137. package/src/system-agents/helper-agent-options.ts +1 -1
  138. package/src/system-agents/memory-reranker.agent.ts +1 -1
  139. package/src/system-agents/memory.agent.ts +1 -1
  140. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  141. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  142. package/src/system-agents/skill-extractor.agent.ts +1 -1
  143. package/src/system-agents/skill-manager.agent.ts +1 -1
  144. package/src/system-agents/title-generator.agent.ts +1 -1
  145. package/src/tools/execution-plan.tool.ts +28 -17
  146. package/src/tools/fetch-webpage.tool.ts +20 -13
  147. package/src/tools/firecrawl-client.ts +13 -3
  148. package/src/tools/plan-approval.tool.ts +9 -1
  149. package/src/tools/search-web.tool.ts +16 -9
  150. package/src/tools/team-think.tool.ts +2 -2
  151. package/src/utils/async.ts +15 -6
  152. package/src/utils/errors.ts +27 -15
  153. package/src/workers/bootstrap.ts +25 -48
  154. package/src/workers/organization-learning.worker.ts +1 -1
  155. package/src/workers/regular-chat-memory-digest.runner.ts +25 -15
  156. package/src/workers/worker-utils.ts +20 -2
  157. package/src/config/search.ts +0 -3
  158. package/src/runtime/agent-types.ts +0 -1
@@ -6,6 +6,7 @@ import type {
6
6
  } from '@lota-sdk/shared'
7
7
  import { PlanNodeAttemptSchema, PlanNodeRunSchema } from '@lota-sdk/shared'
8
8
  import { Context, Schema, Effect, Layer } from 'effect'
9
+ import type { z } from 'zod'
9
10
 
10
11
  import { aiLogger } from '../../config/logger'
11
12
  import type { RecordIdInput } from '../../db/record-id'
@@ -20,6 +21,7 @@ import { GeneratedDocumentStorageServiceTag } from '../../storage/generated-docu
20
21
  import { nowDate } from '../../utils/date-time'
21
22
  import { toError } from '../../utils/errors'
22
23
  import { ArtifactServiceTag } from '../artifact.service'
24
+ import { BackgroundWorkService } from '../background-work.service'
23
25
  import { FeedbackLoopServiceTag } from '../feedback-loop.service'
24
26
  import { InstitutionalMemoryServiceTag } from '../institutional-memory.service'
25
27
  import { QualityMetricsServiceTag } from '../quality-metrics.service'
@@ -97,6 +99,17 @@ function fromPromise<A>(thunk: () => PromiseLike<A> | Effect.Effect<A, unknown>)
97
99
  )
98
100
  }
99
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
+
100
113
  function withDatabaseTransactionEffect<A, E, R>(
101
114
  databaseService: Context.Service.Shape<typeof DatabaseServiceTag>,
102
115
  run: (tx: DatabaseTransaction) => Effect.Effect<A, E, R>,
@@ -310,7 +323,7 @@ function persistNodeResultAttemptEffect(
310
323
  issues: [...submission.validation.blocking, ...submission.validation.warnings],
311
324
  })
312
325
 
313
- void (yield* fromPromise(() =>
326
+ const updatedAttemptRow = yield* fromPromise(() =>
314
327
  tx
315
328
  .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
316
329
  .content({
@@ -327,7 +340,8 @@ function persistNodeResultAttemptEffect(
327
340
  ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
328
341
  })
329
342
  .output('after'),
330
- ).pipe(Effect.map((row) => PlanNodeAttemptSchema.parse(row))))
343
+ )
344
+ void (yield* parseRowOrFail(PlanNodeAttemptSchema, updatedAttemptRow, 'plan node attempt'))
331
345
 
332
346
  const publishedArtifacts =
333
347
  submission.validation.blocking.length > 0
@@ -351,21 +365,20 @@ function persistNodeResultAttemptEffect(
351
365
  artifacts: publishedArtifacts,
352
366
  })
353
367
 
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
- ),
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'),
368
380
  )
381
+ const nextNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, nextNodeRunRow, 'plan node run')
369
382
 
370
383
  const nodeRuns = yield* context.planRunService.listNodeRuns(submission.run.id)
371
384
 
@@ -391,24 +404,23 @@ function handleRetryNodeResultEffect(
391
404
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
392
405
 
393
406
  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
- ),
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'),
411
422
  )
423
+ const retryNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, retryNodeRunRow, 'plan node run')
412
424
 
413
425
  yield* emitEvent({
414
426
  tx,
@@ -477,22 +489,21 @@ function handleHumanReviewNodeResultEffect(
477
489
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
478
490
 
479
491
  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
- ),
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'),
495
505
  )
506
+ const awaitingHumanNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, awaitingHumanNodeRunRow, 'plan node run')
496
507
 
497
508
  const approval = yield* context.planApprovalService
498
509
  .createPendingApproval({
@@ -561,21 +572,20 @@ function handleReplanNodeResultEffect(
561
572
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
562
573
 
563
574
  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
- ),
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'),
578
587
  )
588
+ const blockedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, blockedNodeRunRow, 'plan node run')
579
589
 
580
590
  const blockedRun = yield* replaceRun(tx, submission.run, {
581
591
  status: 'blocked',
@@ -629,22 +639,21 @@ function handleFailedNodeResultEffect(
629
639
  const { tx, emittedEvents, submission, persisted, emittedBy } = params
630
640
 
631
641
  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
- ),
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'),
647
655
  )
656
+ const failedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, failedNodeRunRow, 'plan node run')
648
657
 
649
658
  const failedRun = yield* replaceRun(tx, submission.run, {
650
659
  status: 'failed',
@@ -735,24 +744,23 @@ function handleSuccessfulNodeResultEffect(
735
744
  const { tx, emittedEvents, submission, persisted, emittedBy, result } = params
736
745
 
737
746
  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
- ),
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'),
755
762
  )
763
+ const completedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, completedNodeRunRow, 'plan node run')
756
764
 
757
765
  yield* emitEvent({
758
766
  tx,
@@ -949,7 +957,7 @@ function persistHumanNodeResponseAttemptEffect(
949
957
  issues: [...submission.validation.blocking, ...submission.validation.warnings],
950
958
  })
951
959
 
952
- void (yield* fromPromise(() =>
960
+ const updatedHumanAttemptRow = yield* fromPromise(() =>
953
961
  tx
954
962
  .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
955
963
  .content({
@@ -966,27 +974,27 @@ function persistHumanNodeResponseAttemptEffect(
966
974
  ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
967
975
  })
968
976
  .output('after'),
969
- ).pipe(Effect.map((row) => PlanNodeAttemptSchema.parse(row))))
977
+ )
978
+ void (yield* parseRowOrFail(PlanNodeAttemptSchema, updatedHumanAttemptRow, 'plan node attempt'))
970
979
 
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
- ),
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'),
989
996
  )
997
+ const nextNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, nextNodeRunRow, 'plan node run')
990
998
 
991
999
  const nodeRuns = replaceNodeRunInList(yield* context.planRunService.listNodeRuns(submission.run.id), nextNodeRun)
992
1000
 
@@ -1105,8 +1113,7 @@ function handleAcceptedHumanNodeResponseEffect(
1105
1113
  })
1106
1114
  }
1107
1115
 
1108
- /** @lintignore */
1109
- function submitNodeResult(
1116
+ const submitNodeResultEffect = Effect.fn('PlanExecutor.submitNodeResult')(function* (
1110
1117
  context: PlanExecutorContext,
1111
1118
  params: {
1112
1119
  threadId: RecordIdInput
@@ -1115,7 +1122,7 @@ function submitNodeResult(
1115
1122
  emittedBy: string
1116
1123
  result: PlanNodeResultSubmission
1117
1124
  },
1118
- ): Promise<ExecutionPlanToolResultData> {
1125
+ ) {
1119
1126
  const {
1120
1127
  databaseService,
1121
1128
  generatedDocumentStorageService,
@@ -1124,55 +1131,58 @@ function submitNodeResult(
1124
1131
  planRunService,
1125
1132
  } = context
1126
1133
 
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
- })
1134
+ const submission = yield* loadNodeResultSubmissionContextEffect(context, params)
1135
+ const publishedArtifactStorageKeys: string[] = []
1144
1136
 
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, {
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, {
1156
1153
  tx,
1157
1154
  emittedEvents,
1158
1155
  submission,
1159
1156
  persisted,
1160
1157
  emittedBy: params.emittedBy,
1161
- result: params.result,
1162
1158
  })
1163
- }),
1164
- }).pipe(
1165
- Effect.tapError(() =>
1166
- Effect.forEach(publishedArtifactStorageKeys, (storageKey) =>
1167
- generatedDocumentStorageService.deleteTextArtifact(storageKey).pipe(Effect.ignore),
1168
- ).pipe(Effect.asVoid),
1169
- ),
1170
- )
1159
+ }
1171
1160
 
1172
- const orgId = recordIdToString(submission.run.organizationId, TABLES.ORGANIZATION)
1173
- const runIdStr = recordIdToString(submission.run.id, TABLES.PLAN_RUN)
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
+ )
1174
1178
 
1175
- yield* fromPromise(() =>
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(() =>
1176
1186
  planCompletionSideEffects.runPlanNodeCompletionSideEffects({
1177
1187
  runId: runIdStr,
1178
1188
  organizationId: orgId,
@@ -1192,31 +1202,50 @@ function submitNodeResult(
1192
1202
  aiLogger.warn`Failed to record node completion metrics for run ${runIdStr} node ${params.nodeId}: ${error.message}`
1193
1203
  }),
1194
1204
  ),
1195
- Effect.forkDetach,
1196
- )
1205
+ ),
1206
+ 'plan-executor.recordNodeCompletionMetrics',
1207
+ )
1197
1208
 
1198
- const updatedRun = yield* planRunService.getRunById(submission.run.id)
1199
- if (updatedRun.status === 'completed') {
1200
- yield* fromPromise(() =>
1209
+ const updatedRun = yield* planRunService.getRunById(submission.run.id)
1210
+ if (updatedRun.status === 'completed') {
1211
+ yield* background.run(
1212
+ fromPromise(() =>
1201
1213
  planCompletionSideEffects.runPlanCompletionSideEffectsSafely({ runId: runIdStr, organizationId: orgId }),
1202
1214
  ).pipe(
1203
- Effect.catchTag('PlanExecutorInternalError', () => Effect.void),
1204
- Effect.forkDetach,
1205
- )
1206
- }
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
+ }
1207
1224
 
1208
- const snapshot = yield* serializeRunFull(planRunService, updatedRun)
1225
+ const snapshot = yield* serializeRunFull(planRunService, updatedRun)
1209
1226
 
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
- }
1227
+ return buildExecutionPlanToolResult({
1228
+ action: 'node-result-submitted',
1229
+ plan: snapshot,
1230
+ message: `Submitted result for node "${submission.nodeSpec.label}".`,
1231
+ })
1232
+ })
1217
1233
 
1218
1234
  /** @lintignore */
1219
- function submitHumanNodeResponse(
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* (
1220
1249
  context: PlanExecutorContext,
1221
1250
  params: {
1222
1251
  threadId: RecordIdInput
@@ -1225,192 +1254,302 @@ function submitHumanNodeResponse(
1225
1254
  response: HumanNodeResponsePayload
1226
1255
  approvalMessageId?: string
1227
1256
  },
1228
- ): Promise<SerializableExecutionPlan | null> {
1257
+ ) {
1229
1258
  const { databaseService, planEventDeliveryService } = context
1230
1259
 
1231
- return Effect.gen(function* () {
1232
- const submission = yield* loadHumanNodeResponseContextEffect(context, params)
1233
- if (!submission) {
1234
- return null
1235
- }
1260
+ const submission = yield* loadHumanNodeResponseContextEffect(context, params)
1261
+ if (!submission) {
1262
+ return null
1263
+ }
1236
1264
 
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
- })
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
+ })
1249
1277
 
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, {
1278
+ if (submission.validation.blocking.length > 0) {
1279
+ return yield* handleBlockedHumanNodeResponseEffect(context, {
1261
1280
  tx,
1262
1281
  emittedEvents,
1263
1282
  submission,
1264
1283
  persisted,
1265
1284
  respondedBy: params.respondedBy,
1266
1285
  })
1267
- }),
1268
- })
1269
- }).pipe(runPromise)
1270
- }
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
+ })
1271
1298
 
1272
1299
  /** @lintignore */
1273
- function resumeRun(
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* (
1274
1314
  context: PlanExecutorContext,
1275
1315
  params: { threadId: RecordIdInput; runId: string; emittedBy: string },
1276
- ): Promise<ExecutionPlanToolResultData> {
1316
+ ) {
1277
1317
  const { databaseService, planEventDeliveryService, planRunService } = context
1278
1318
 
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
- })
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
+ })
1322
1362
 
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
- })
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
+ })
1337
1377
 
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
- })
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'))
1369
1409
 
1370
- return buildExecutionPlanToolResult({
1371
- action: 'run-resumed',
1372
- plan: snapshot,
1373
- message: `Resumed execution run "${snapshot.title}".`,
1374
- })
1375
- }).pipe(runPromise)
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)
1376
1423
  }
1377
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
+
1378
1458
  /** @lintignore */
1379
1459
  function transitionNodeToRunning(
1380
1460
  context: PlanExecutorContext,
1381
1461
  params: { runId: string; nodeId: string },
1382
1462
  ): Promise<void> {
1383
- const { databaseService, planRunService } = context
1463
+ return transitionNodeToRunningEffect(context, params).pipe(runPromise)
1464
+ }
1384
1465
 
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
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
1389
1478
 
1390
- yield* withDatabaseTransactionEffect(databaseService, (tx) =>
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) =>
1391
1492
  Effect.gen(function* () {
1392
- const runningNodeRun = PlanNodeRunSchema.parse(
1393
- yield* fromPromise(() =>
1493
+ let blockedNodeRun: PlanNodeRunRecord
1494
+ if (nodeRun.status === 'blocked') {
1495
+ blockedNodeRun = nodeRun
1496
+ } else {
1497
+ const blockedNodeRunRow = yield* fromPromise(() =>
1394
1498
  tx
1395
1499
  .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1396
- .content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? nowDate() }))
1500
+ .content(
1501
+ toNodeRunData(nodeRun, {
1502
+ status: 'blocked',
1503
+ blockedReason: params.message,
1504
+ failureClass: params.failureClass,
1505
+ }),
1506
+ )
1397
1507
  .output('after'),
1398
- ),
1399
- )
1508
+ )
1509
+ blockedNodeRun = yield* parseRowOrFail(PlanNodeRunSchema, blockedNodeRunRow, 'plan node run')
1510
+ }
1400
1511
 
1401
- const nodeRuns = yield* planRunService.listNodeRuns(run.id)
1402
- yield* replaceRun(tx, run, {
1403
- status: 'running',
1404
- currentNodeId: runningNodeRun.nodeId,
1512
+ const blockedRun = yield* replaceRun(tx, run, {
1513
+ status: 'blocked',
1514
+ currentNodeId: blockedNodeRun.nodeId,
1405
1515
  waitingNodeId: null,
1406
- readyNodeIds: nodeRuns
1407
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
1408
- .map((candidate) => candidate.nodeId),
1516
+ readyNodeIds: [],
1517
+ failureCount: run.failureCount + 1,
1409
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)
1410
1550
  }),
1411
- )
1412
- }).pipe(runPromise)
1413
- }
1551
+ }).pipe(Effect.withSpan('PlanExecutor.blockNodeOnDispatchFailure.persistTransaction'))
1552
+ })
1414
1553
 
1415
1554
  /** @lintignore */
1416
1555
  function blockNodeOnDispatchFailure(
@@ -1424,163 +1563,96 @@ function blockNodeOnDispatchFailure(
1424
1563
  failureClass: PlanFailureClass
1425
1564
  },
1426
1565
  ): Promise<SerializableExecutionPlan> {
1427
- const { databaseService, planEventDeliveryService, planRunService } = context
1428
-
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
- })
1467
-
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
- })
1481
-
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)
1566
+ return blockNodeOnDispatchFailureEffect(context, params).pipe(runPromise)
1501
1567
  }
1502
1568
 
1503
- /** @lintignore */
1504
- function promoteDelayedNode(
1569
+ const promoteDelayedNodeEffect = Effect.fn('PlanExecutor.promoteDelayedNode')(function* (
1505
1570
  context: PlanExecutorContext,
1506
1571
  params: { runId: string; nodeId: string; emittedBy: string },
1507
- ): Promise<void> {
1572
+ ) {
1508
1573
  const { databaseService, planEventDeliveryService, planRunService } = context
1509
1574
 
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
- }
1575
+ const run = yield* planRunService.getRunById(params.runId)
1576
+ if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
1577
+ return
1578
+ }
1515
1579
 
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
- )
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
1538
1584
 
1539
- const updatedNodeRuns = nodeRuns.map((candidate) =>
1540
- candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
1541
- )
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'))
1542
1590
 
1543
- const currentNodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
1544
- yield* emitEvent({
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, {
1545
1624
  tx,
1546
1625
  run,
1547
1626
  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.`,
1627
+ nodeSpecs,
1628
+ nodeRuns: updatedNodeRuns,
1629
+ artifacts,
1553
1630
  emittedBy: params.emittedBy,
1554
1631
  capturedEvents: emittedEvents,
1555
- })
1632
+ }),
1633
+ )
1556
1634
 
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
- )
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
+ })
1569
1649
 
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)
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)
1584
1656
  }
1585
1657
 
1586
1658
  export function makePlanExecutorService(deps: PlanExecutorDeps) {
@@ -1611,14 +1683,25 @@ export function makePlanExecutorService(deps: PlanExecutorDeps) {
1611
1683
 
1612
1684
  return {
1613
1685
  submitNodeResult: (params: Parameters<typeof submitNodeResult>[1]) => submitNodeResult(context, params),
1686
+ submitNodeResultEffect: (params: Parameters<typeof submitNodeResultEffect>[1]) =>
1687
+ submitNodeResultEffect(context, params),
1614
1688
  submitHumanNodeResponse: (params: Parameters<typeof submitHumanNodeResponse>[1]) =>
1615
1689
  submitHumanNodeResponse(context, params),
1690
+ submitHumanNodeResponseEffect: (params: Parameters<typeof submitHumanNodeResponseEffect>[1]) =>
1691
+ submitHumanNodeResponseEffect(context, params),
1616
1692
  resumeRun: (params: Parameters<typeof resumeRun>[1]) => resumeRun(context, params),
1693
+ resumeRunEffect: (params: Parameters<typeof resumeRunEffect>[1]) => resumeRunEffect(context, params),
1617
1694
  transitionNodeToRunning: (params: Parameters<typeof transitionNodeToRunning>[1]) =>
1618
1695
  transitionNodeToRunning(context, params),
1696
+ transitionNodeToRunningEffect: (params: Parameters<typeof transitionNodeToRunningEffect>[1]) =>
1697
+ transitionNodeToRunningEffect(context, params),
1619
1698
  blockNodeOnDispatchFailure: (params: Parameters<typeof blockNodeOnDispatchFailure>[1]) =>
1620
1699
  blockNodeOnDispatchFailure(context, params),
1700
+ blockNodeOnDispatchFailureEffect: (params: Parameters<typeof blockNodeOnDispatchFailureEffect>[1]) =>
1701
+ blockNodeOnDispatchFailureEffect(context, params),
1621
1702
  promoteDelayedNode: (params: Parameters<typeof promoteDelayedNode>[1]) => promoteDelayedNode(context, params),
1703
+ promoteDelayedNodeEffect: (params: Parameters<typeof promoteDelayedNodeEffect>[1]) =>
1704
+ promoteDelayedNodeEffect(context, params),
1622
1705
  syncRunGraph: (params: Parameters<typeof syncRunGraph>[1]) => syncRunGraph(context, params),
1623
1706
  resolveFailureAction,
1624
1707
  buildResolvedInput,
@@ -1626,7 +1709,7 @@ export function makePlanExecutorService(deps: PlanExecutorDeps) {
1626
1709
  }
1627
1710
 
1628
1711
  export class PlanExecutorServiceTag extends Context.Service<PlanExecutorServiceTag, PlanExecutorService>()(
1629
- 'PlanExecutorService',
1712
+ '@lota-sdk/core/PlanExecutorService',
1630
1713
  ) {}
1631
1714
 
1632
1715
  export const PlanExecutorServiceLive = Layer.effect(