@lota-sdk/core 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,1041 @@
1
+ import type {
2
+ ChatMessage,
3
+ ExecutionPlanQueryArgs,
4
+ PlanDraft,
5
+ PlanRunRecord,
6
+ SerializableExecutionPlan,
7
+ SubmitPlanTurnResultArgs,
8
+ SubmitExecutionNodeResultArgs,
9
+ } from '@lota-sdk/shared'
10
+ import { PlanDraftSchema, PlanRunSchema, PlanSpecSchema } from '@lota-sdk/shared'
11
+ import { Context, Schema, Effect, Layer, Match } from 'effect'
12
+ import { RecordId } from 'surrealdb'
13
+
14
+ import type { RecordIdInput } from '../../db/record-id'
15
+ import { ensureRecordId, recordIdToString } from '../../db/record-id'
16
+ import type { SurrealDBService } from '../../db/service'
17
+ import { TABLES } from '../../db/tables'
18
+ import { BadRequestError } from '../../effect/errors'
19
+ import { makeEffectTryPromiseWithMessage } from '../../effect/helpers'
20
+ import { DatabaseServiceTag } from '../../effect/services'
21
+ import { extractMessageText } from '../../runtime/thread-chat-helpers'
22
+ import { nowDate } from '../../utils/date-time'
23
+ import type { makeOwnershipDispatcherService } from '../ownership-dispatcher.service'
24
+ import { OwnershipDispatcherServiceTag } from '../ownership-dispatcher.service'
25
+ import type { makePlanBuilderService } from '../plan/plan-builder.service'
26
+ import { PlanBuilderServiceTag } from '../plan/plan-builder.service'
27
+ import type { makePlanCheckpointService } from '../plan/plan-checkpoint.service'
28
+ import { PlanCheckpointServiceTag } from '../plan/plan-checkpoint.service'
29
+ import type { makePlanCompilerService } from '../plan/plan-compiler.service'
30
+ import { PlanCompilerServiceTag } from '../plan/plan-compiler.service'
31
+ import type { makePlanEventDeliveryService } from '../plan/plan-event-delivery.service'
32
+ import { PlanEventDeliveryServiceTag } from '../plan/plan-event-delivery.service'
33
+ import type { HumanNodeResponsePayload } from '../plan/plan-executor-helpers'
34
+ import { attachCheckpoint, emitEvent, saveCheckpoint } from '../plan/plan-executor-persistence'
35
+ import type { makePlanExecutorService } from '../plan/plan-executor.service'
36
+ import { PlanExecutorServiceTag } from '../plan/plan-executor.service'
37
+ import { buildExecutionPlanToolResult, toRunData } from '../plan/plan-run-data'
38
+ import { serializeRunFull } from '../plan/plan-run-serialization'
39
+ import type { makePlanRunService } from '../plan/plan-run.service'
40
+ import { PlanRunServiceTag } from '../plan/plan-run.service'
41
+ import type { makePlanSchedulerService } from '../plan/plan-scheduler.service'
42
+ import { PlanSchedulerServiceTag } from '../plan/plan-scheduler.service'
43
+ import { withTransactionAndEventsEffect } from '../plan/plan-transaction-events'
44
+ import type { makePlanValidatorService } from '../plan/plan-validator.service'
45
+ import { PlanValidatorServiceTag } from '../plan/plan-validator.service'
46
+ import { buildApprovalResponseFromMessages } from './execution-plan-approval'
47
+ import {
48
+ aggregateBlockingIssues,
49
+ hasCrossThreadSourceContext,
50
+ isPlanVisibleInThreadContext,
51
+ } from './execution-plan-context'
52
+ import { createInitializedRunGraph } from './execution-plan-graph'
53
+ import { attachPlanScheduleIfNeeded } from './execution-plan-schedule'
54
+ import { buildCompiledSpecCreateData, toSpecData } from './execution-plan-spec'
55
+
56
+ interface ExecutionPlanDeps {
57
+ db: SurrealDBService
58
+ ownershipDispatcher: ReturnType<typeof makeOwnershipDispatcherService>
59
+ planBuilder: ReturnType<typeof makePlanBuilderService>
60
+ planCheckpoint: ReturnType<typeof makePlanCheckpointService>
61
+ planCompiler: ReturnType<typeof makePlanCompilerService>
62
+ planEventDelivery: ReturnType<typeof makePlanEventDeliveryService>
63
+ planExecutor: ReturnType<typeof makePlanExecutorService>
64
+ planRun: ReturnType<typeof makePlanRunService>
65
+ planScheduler: ReturnType<typeof makePlanSchedulerService>
66
+ planValidator: ReturnType<typeof makePlanValidatorService>
67
+ }
68
+
69
+ type PreparedPlanDraft = Parameters<ExecutionPlanDeps['planValidator']['validateDraft']>[0]
70
+
71
+ function saveCheckpointWithService(
72
+ deps: ExecutionPlanDeps,
73
+ params: Omit<Parameters<typeof saveCheckpoint>[0], 'planCheckpointService'>,
74
+ ) {
75
+ return saveCheckpoint({ ...params, planCheckpointService: deps.planCheckpoint })
76
+ }
77
+
78
+ class ExecutionPlanServiceError extends Schema.TaggedErrorClass<ExecutionPlanServiceError>()(
79
+ 'ExecutionPlanServiceError',
80
+ { message: Schema.String, cause: Schema.optional(Schema.Defect) },
81
+ ) {}
82
+
83
+ const effectTryPromise = makeEffectTryPromiseWithMessage(
84
+ (message, cause) => new ExecutionPlanServiceError({ message, cause }),
85
+ )
86
+
87
+ function toSerializablePlanEffect(
88
+ deps: ExecutionPlanDeps,
89
+ run: PlanRunRecord,
90
+ options?: Partial<ExecutionPlanQueryArgs>,
91
+ ) {
92
+ return deps.planRun
93
+ .toSerializablePlan(run, {
94
+ includeEvents: options?.includeEvents,
95
+ includeArtifacts: options?.includeArtifacts,
96
+ includeApprovals: options?.includeApprovals,
97
+ includeCheckpoints: options?.includeCheckpoints,
98
+ includeValidationIssues: options?.includeValidationIssues,
99
+ })
100
+ .pipe(
101
+ Effect.mapError(
102
+ (cause) => new ExecutionPlanServiceError({ message: 'Failed to serialize execution plan.', cause }),
103
+ ),
104
+ )
105
+ }
106
+
107
+ function serializeRunsEffect(
108
+ deps: ExecutionPlanDeps,
109
+ runs: PlanRunRecord[],
110
+ options?: Partial<ExecutionPlanQueryArgs>,
111
+ ) {
112
+ return Effect.all(runs.map((run) => toSerializablePlanEffect(deps, run, options)))
113
+ }
114
+
115
+ function assertDispatchExecutorsEffect(
116
+ deps: ExecutionPlanDeps,
117
+ preparedDraft: PreparedPlanDraft,
118
+ ): Effect.Effect<void, BadRequestError> {
119
+ return Effect.gen(function* () {
120
+ const issues = deps.ownershipDispatcher.validateDraftExecutors(preparedDraft)
121
+ if (issues.length > 0) {
122
+ return yield* new BadRequestError({ message: `Plan draft failed validation: ${aggregateBlockingIssues(issues)}` })
123
+ }
124
+ })
125
+ }
126
+
127
+ function finalizePlanSnapshotEffect(deps: ExecutionPlanDeps, params: { runId: RecordIdInput; emittedBy: string }) {
128
+ return deps.ownershipDispatcher
129
+ .dispatchRunToStableBoundary({ runId: params.runId, emittedBy: params.emittedBy })
130
+ .pipe(
131
+ Effect.mapError(
132
+ (cause) => new ExecutionPlanServiceError({ message: 'Failed to finalize execution plan snapshot.', cause }),
133
+ ),
134
+ )
135
+ }
136
+
137
+ function hasActivePlanEffect(deps: ExecutionPlanDeps, threadId: RecordIdInput) {
138
+ return Effect.gen(function* () {
139
+ const run = yield* deps.planRun.getActiveRunRecord(threadId)
140
+ return run !== null
141
+ })
142
+ }
143
+
144
+ function getActivePlanForThreadEffect(
145
+ deps: ExecutionPlanDeps,
146
+ threadId: RecordIdInput,
147
+ options?: Partial<ExecutionPlanQueryArgs> & { runId?: RecordIdInput },
148
+ ) {
149
+ return Effect.gen(function* () {
150
+ const plans = yield* getActivePlansForThreadEffect(deps, threadId, options)
151
+ return plans[0] ?? null
152
+ })
153
+ }
154
+
155
+ function getActivePlansForThreadEffect(
156
+ deps: ExecutionPlanDeps,
157
+ threadId: RecordIdInput,
158
+ options?: Partial<ExecutionPlanQueryArgs> & { runId?: RecordIdInput },
159
+ ) {
160
+ return Effect.gen(function* () {
161
+ const runId = options?.runId
162
+ if (runId) {
163
+ const run = yield* deps.planRun.getRunById(runId)
164
+ const plan = yield* toSerializablePlanEffect(deps, run, options)
165
+ return [plan]
166
+ }
167
+
168
+ const runs = yield* deps.planRun.getActiveRunRecords(threadId)
169
+ if (runs.length === 0) return []
170
+ return yield* serializeRunsEffect(deps, runs, options)
171
+ })
172
+ }
173
+
174
+ function getPlansCreatedInContextEffect(params: {
175
+ deps: ExecutionPlanDeps
176
+ organizationId: RecordIdInput
177
+ sourceThreadId?: RecordIdInput
178
+ createdByAgentId?: string
179
+ statuses?: ReadonlyArray<PlanRunRecord['status']>
180
+ includeEvents?: boolean
181
+ includeArtifacts?: boolean
182
+ includeApprovals?: boolean
183
+ includeCheckpoints?: boolean
184
+ includeValidationIssues?: boolean
185
+ }) {
186
+ return Effect.gen(function* () {
187
+ const runs = yield* params.deps.planRun.getRunsCreatedInContext({
188
+ organizationId: params.organizationId,
189
+ sourceThreadId: params.sourceThreadId,
190
+ createdByAgentId: params.createdByAgentId,
191
+ statuses: params.statuses,
192
+ })
193
+ if (runs.length === 0) return []
194
+ return yield* serializeRunsEffect(params.deps, runs, params)
195
+ })
196
+ }
197
+
198
+ function listActivePlanSummariesEffect(deps: ExecutionPlanDeps, threadId: RecordIdInput) {
199
+ return Effect.gen(function* () {
200
+ const runs = yield* deps.planRun.getActiveRunRecords(threadId)
201
+ const plans = yield* Effect.all(
202
+ runs.map((run) =>
203
+ Effect.gen(function* () {
204
+ const [spec, nodeRuns] = yield* Effect.all([
205
+ deps.planRun.getPlanSpecById(run.planSpecId),
206
+ deps.planRun.listNodeRuns(run.id),
207
+ ])
208
+
209
+ return {
210
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
211
+ title: spec.title,
212
+ status: run.status,
213
+ objective: spec.objective,
214
+ nodeCount: nodeRuns.length,
215
+ completedCount: nodeRuns.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
216
+ .length,
217
+ failedCount: nodeRuns.filter((nodeRun) => nodeRun.status === 'failed').length,
218
+ }
219
+ }),
220
+ ),
221
+ )
222
+
223
+ return { plans, totalCount: plans.length }
224
+ })
225
+ }
226
+
227
+ function getActivePlanToolResultEffect(
228
+ deps: ExecutionPlanDeps,
229
+ params: {
230
+ threadId: RecordIdInput
231
+ runId?: string
232
+ includeEvents?: boolean
233
+ includeArtifacts?: boolean
234
+ includeApprovals?: boolean
235
+ includeCheckpoints?: boolean
236
+ includeValidationIssues?: boolean
237
+ },
238
+ ) {
239
+ const serializeOptions = {
240
+ includeEvents: params.includeEvents,
241
+ includeArtifacts: params.includeArtifacts,
242
+ includeApprovals: params.includeApprovals,
243
+ includeCheckpoints: params.includeCheckpoints,
244
+ includeValidationIssues: params.includeValidationIssues,
245
+ }
246
+
247
+ return Effect.gen(function* () {
248
+ const runId = params.runId
249
+ if (runId) {
250
+ const run = yield* deps.planRun.getRunById(runId)
251
+ const plan = yield* toSerializablePlanEffect(deps, run, serializeOptions)
252
+ return buildExecutionPlanToolResult({ action: 'loaded', plan, message: `Loaded execution run "${plan.title}".` })
253
+ }
254
+
255
+ const runs = yield* deps.planRun.getActiveRunRecords(params.threadId)
256
+ if (runs.length === 0) {
257
+ return buildExecutionPlanToolResult({ action: 'none', plan: null, message: 'No active execution run.' })
258
+ }
259
+
260
+ const plan = yield* toSerializablePlanEffect(deps, runs[0], serializeOptions)
261
+ return buildExecutionPlanToolResult({
262
+ action: 'loaded',
263
+ plan,
264
+ message:
265
+ runs.length === 1
266
+ ? `Loaded execution run "${plan.title}".`
267
+ : `Loaded ${runs.length} active execution runs. Showing most recent: "${plan.title}".`,
268
+ })
269
+ })
270
+ }
271
+
272
+ function createPlanEffect(
273
+ deps: ExecutionPlanDeps,
274
+ params: {
275
+ organizationId: RecordIdInput
276
+ threadId: RecordIdInput
277
+ sourceThreadId?: RecordIdInput
278
+ leadAgentId: string
279
+ createdByAgentId?: string
280
+ requireApproval?: boolean
281
+ input: PlanDraft
282
+ },
283
+ ) {
284
+ return Effect.gen(function* () {
285
+ const databaseService = deps.db
286
+ const requireApproval =
287
+ params.requireApproval ?? hasCrossThreadSourceContext(params.sourceThreadId, params.threadId)
288
+ const preparedDraft = deps.planBuilder.prepareDraft(PlanDraftSchema.parse(params.input))
289
+ const validation = deps.planValidator.validateDraft(preparedDraft)
290
+ if (validation.blocking.length > 0) {
291
+ return yield* new BadRequestError({
292
+ message: `Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`,
293
+ })
294
+ }
295
+ yield* assertDispatchExecutorsEffect(deps, preparedDraft)
296
+ const compiled = yield* deps.planCompiler.compile(preparedDraft)
297
+
298
+ const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
299
+ const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
300
+
301
+ yield* withTransactionAndEventsEffect({
302
+ db: databaseService,
303
+ planEventDeliveryService: deps.planEventDelivery,
304
+ run: (tx, emittedEvents) =>
305
+ Effect.gen(function* () {
306
+ const createdSpec = yield* effectTryPromise(
307
+ () =>
308
+ tx
309
+ .create(specId)
310
+ .content(
311
+ buildCompiledSpecCreateData({
312
+ organizationId: params.organizationId,
313
+ threadId: params.threadId,
314
+ leadAgentId: params.leadAgentId,
315
+ compiled,
316
+ version: 1,
317
+ }),
318
+ )
319
+ .output('after'),
320
+ 'Failed to create execution plan spec.',
321
+ )
322
+ const spec = PlanSpecSchema.parse(createdSpec)
323
+
324
+ yield* effectTryPromise(
325
+ () =>
326
+ createInitializedRunGraph({
327
+ tx,
328
+ runId,
329
+ spec,
330
+ organizationId: params.organizationId,
331
+ threadId: params.threadId,
332
+ sourceThreadId: params.sourceThreadId,
333
+ leadAgentId: params.leadAgentId,
334
+ createdByAgentId: params.createdByAgentId,
335
+ requireApproval,
336
+ nodes: compiled.nodes,
337
+ emittedEvents,
338
+ createdEventType: 'plan-created',
339
+ createdEventMessage: `Created execution plan "${spec.title}".`,
340
+ createdEventDetail: { title: spec.title, objective: spec.objective },
341
+ checkpointReason: 'plan-created',
342
+ planExecutor: deps.planExecutor,
343
+ }),
344
+ 'Failed to initialize execution plan graph.',
345
+ )
346
+ }),
347
+ })
348
+
349
+ if (!requireApproval) {
350
+ yield* effectTryPromise(
351
+ () =>
352
+ attachPlanScheduleIfNeeded({
353
+ db: databaseService,
354
+ planRunService: deps.planRun,
355
+ planSchedulerService: deps.planScheduler,
356
+ organizationId: params.organizationId,
357
+ threadId: params.threadId,
358
+ runId,
359
+ planSpecId: specId,
360
+ }),
361
+ 'Failed to attach execution plan schedule.',
362
+ )
363
+ }
364
+
365
+ const plan = yield* finalizePlanSnapshotEffect(deps, { runId, emittedBy: params.leadAgentId })
366
+ return buildExecutionPlanToolResult({ action: 'created', plan, message: `Created execution plan "${plan.title}".` })
367
+ })
368
+ }
369
+
370
+ function replacePlanEffect(
371
+ deps: ExecutionPlanDeps,
372
+ params: {
373
+ threadId: RecordIdInput
374
+ organizationId: RecordIdInput
375
+ leadAgentId: string
376
+ createdByAgentId?: string
377
+ input: PlanDraft & { runId: string; reason: string; requireApproval?: boolean }
378
+ },
379
+ ) {
380
+ return Effect.gen(function* () {
381
+ const databaseService = deps.db
382
+ const activeRun = yield* deps.planRun.getRunById(params.input.runId)
383
+ const resolvedThreadId = activeRun.threadId
384
+ const activeRuns = yield* deps.planRun.getActiveRunRecords(resolvedThreadId)
385
+ if (activeRuns.length === 0) {
386
+ return yield* new BadRequestError({ message: 'No active execution run exists for this thread.' })
387
+ }
388
+ if (!activeRuns.some((run) => recordIdToString(run.id, TABLES.PLAN_RUN) === params.input.runId)) {
389
+ return yield* new BadRequestError({ message: 'Only an active execution run can be replaced.' })
390
+ }
391
+
392
+ const activeSpec = yield* deps.planRun.getPlanSpecById(activeRun.planSpecId)
393
+ const { runId: _runId, reason: _reason, requireApproval: requestedRequireApproval, ...draftInput } = params.input
394
+ const preparedDraft = deps.planBuilder.prepareDraft(PlanDraftSchema.parse(draftInput))
395
+ const validation = deps.planValidator.validateDraft(preparedDraft)
396
+ if (validation.blocking.length > 0) {
397
+ return yield* new BadRequestError({
398
+ message: `Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`,
399
+ })
400
+ }
401
+ yield* assertDispatchExecutorsEffect(deps, preparedDraft)
402
+ const compiled = yield* deps.planCompiler.compile(preparedDraft)
403
+ const requireApproval =
404
+ requestedRequireApproval ?? hasCrossThreadSourceContext(activeRun.sourceThreadId, resolvedThreadId)
405
+
406
+ const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
407
+ const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
408
+
409
+ yield* withTransactionAndEventsEffect({
410
+ db: databaseService,
411
+ planEventDeliveryService: deps.planEventDelivery,
412
+ run: (tx, emittedEvents) =>
413
+ Effect.gen(function* () {
414
+ const updatedSpec = yield* effectTryPromise(
415
+ () =>
416
+ tx
417
+ .update(ensureRecordId(activeSpec.id, TABLES.PLAN_SPEC))
418
+ .content(toSpecData(activeSpec, { status: 'superseded' }))
419
+ .output('after'),
420
+ 'Failed to update superseded execution plan spec.',
421
+ )
422
+ const supersededSpec = PlanSpecSchema.parse(updatedSpec)
423
+
424
+ const updatedRun = yield* effectTryPromise(
425
+ () =>
426
+ tx
427
+ .update(ensureRecordId(activeRun.id, TABLES.PLAN_RUN))
428
+ .content(
429
+ toRunData(activeRun, {
430
+ status: 'aborted',
431
+ currentNodeId: null,
432
+ waitingNodeId: null,
433
+ readyNodeIds: [],
434
+ completedAt: nowDate(),
435
+ }),
436
+ )
437
+ .output('after'),
438
+ 'Failed to abort previous execution run.',
439
+ )
440
+ const abortedRun = PlanRunSchema.parse(updatedRun)
441
+
442
+ const createdSpec = yield* effectTryPromise(
443
+ () =>
444
+ tx
445
+ .create(specId)
446
+ .content(
447
+ buildCompiledSpecCreateData({
448
+ organizationId: params.organizationId,
449
+ threadId: resolvedThreadId,
450
+ leadAgentId: params.leadAgentId,
451
+ compiled,
452
+ version: supersededSpec.version + 1,
453
+ replacedSpecId: supersededSpec.id,
454
+ }),
455
+ )
456
+ .output('after'),
457
+ 'Failed to create replacement execution plan spec.',
458
+ )
459
+ const spec = PlanSpecSchema.parse(createdSpec)
460
+
461
+ yield* effectTryPromise(
462
+ () =>
463
+ createInitializedRunGraph({
464
+ tx,
465
+ runId,
466
+ spec,
467
+ organizationId: params.organizationId,
468
+ threadId: resolvedThreadId,
469
+ sourceThreadId: activeRun.sourceThreadId,
470
+ leadAgentId: params.leadAgentId,
471
+ createdByAgentId: params.createdByAgentId ?? params.leadAgentId,
472
+ requireApproval,
473
+ nodes: compiled.nodes,
474
+ emittedEvents,
475
+ runPatch: { replacedRunId: abortedRun.id },
476
+ createdEventType: 'plan-replaced',
477
+ createdEventMessage: `Replaced execution plan "${activeSpec.title}" with "${spec.title}".`,
478
+ createdEventDetail: {
479
+ reason: params.input.reason,
480
+ replacedRunId: recordIdToString(abortedRun.id, TABLES.PLAN_RUN),
481
+ },
482
+ checkpointReason: 'plan-replaced',
483
+ planExecutor: deps.planExecutor,
484
+ }),
485
+ 'Failed to initialize replacement execution plan graph.',
486
+ )
487
+ }),
488
+ })
489
+
490
+ if (!requireApproval) {
491
+ yield* effectTryPromise(
492
+ () =>
493
+ attachPlanScheduleIfNeeded({
494
+ db: databaseService,
495
+ planRunService: deps.planRun,
496
+ planSchedulerService: deps.planScheduler,
497
+ organizationId: params.organizationId,
498
+ threadId: resolvedThreadId,
499
+ runId,
500
+ planSpecId: specId,
501
+ }),
502
+ 'Failed to attach execution plan schedule.',
503
+ )
504
+ }
505
+
506
+ const plan = yield* finalizePlanSnapshotEffect(deps, { runId, emittedBy: params.leadAgentId })
507
+ return buildExecutionPlanToolResult({
508
+ action: 'replaced',
509
+ plan,
510
+ message: `Replaced execution plan with "${plan.title}".`,
511
+ })
512
+ })
513
+ }
514
+
515
+ function submitPlanTurnResultEffect(
516
+ deps: ExecutionPlanDeps,
517
+ params: {
518
+ threadId: RecordIdInput
519
+ runId: string
520
+ nodeId: string
521
+ emittedBy: string
522
+ input: SubmitPlanTurnResultArgs
523
+ },
524
+ ) {
525
+ return Effect.gen(function* () {
526
+ const result = yield* Effect.tryPromise({
527
+ try: () =>
528
+ deps.planExecutor.submitNodeResult({
529
+ threadId: params.threadId,
530
+ runId: params.runId,
531
+ nodeId: params.nodeId,
532
+ emittedBy: params.emittedBy,
533
+ result: params.input,
534
+ }),
535
+ catch: (cause) =>
536
+ new ExecutionPlanServiceError({ message: 'Failed to submit execution plan node result.', cause }),
537
+ })
538
+
539
+ const plan = yield* finalizePlanSnapshotEffect(deps, { runId: params.runId, emittedBy: params.emittedBy })
540
+ return buildExecutionPlanToolResult({
541
+ action: result.action,
542
+ plan,
543
+ message: result.message ?? `Submitted result for node "${params.nodeId}".`,
544
+ })
545
+ })
546
+ }
547
+
548
+ function resumeRunEffect(
549
+ deps: ExecutionPlanDeps,
550
+ params: { threadId: RecordIdInput; emittedBy: string; input: { runId: string } },
551
+ ) {
552
+ return Effect.gen(function* () {
553
+ const result = yield* Effect.tryPromise({
554
+ try: () =>
555
+ deps.planExecutor.resumeRun({
556
+ threadId: params.threadId,
557
+ runId: params.input.runId,
558
+ emittedBy: params.emittedBy,
559
+ }),
560
+ catch: (cause) => new ExecutionPlanServiceError({ message: 'Failed to resume execution run.', cause }),
561
+ })
562
+
563
+ const plan = yield* finalizePlanSnapshotEffect(deps, { runId: params.input.runId, emittedBy: params.emittedBy })
564
+ return buildExecutionPlanToolResult({
565
+ action: result.action,
566
+ plan,
567
+ message: result.message ?? `Resumed execution run "${params.input.runId}".`,
568
+ })
569
+ })
570
+ }
571
+
572
+ function approvePlanEffect(
573
+ deps: ExecutionPlanDeps,
574
+ params: { organizationId: RecordIdInput; threadId: RecordIdInput; runId: RecordIdInput; emittedBy: string },
575
+ ) {
576
+ return Effect.gen(function* () {
577
+ const databaseService = deps.db
578
+ const run = yield* deps.planRun.getRunById(params.runId)
579
+ if (
580
+ recordIdToString(run.organizationId, TABLES.ORGANIZATION) !==
581
+ recordIdToString(params.organizationId, TABLES.ORGANIZATION)
582
+ ) {
583
+ return yield* new BadRequestError({ message: 'Plan run belongs to a different organization.' })
584
+ }
585
+ if (!isPlanVisibleInThreadContext(params.threadId, run)) {
586
+ return yield* new BadRequestError({ message: 'Plan run is not available in this thread context.' })
587
+ }
588
+ if (run.status !== 'pending-approval') {
589
+ return yield* new BadRequestError({ message: 'Only pending-approval plans can be approved.' })
590
+ }
591
+
592
+ const [spec, nodeSpecs, nodeRuns, nextCheckpointSequence] = yield* Effect.all([
593
+ deps.planRun.getPlanSpecById(run.planSpecId),
594
+ deps.planRun.listNodeSpecs(run.planSpecId),
595
+ deps.planRun.listNodeRuns(run.id),
596
+ deps.planRun.getNextCheckpointSequence(run.id),
597
+ ])
598
+
599
+ yield* withTransactionAndEventsEffect({
600
+ db: databaseService,
601
+ planEventDeliveryService: deps.planEventDelivery,
602
+ run: (tx, emittedEvents) =>
603
+ Effect.gen(function* () {
604
+ const updatedRun = yield* effectTryPromise(
605
+ () =>
606
+ tx
607
+ .update(ensureRecordId(run.id, TABLES.PLAN_RUN))
608
+ .content(toRunData(run, { status: 'running', startedAt: run.startedAt ?? nowDate() }))
609
+ .output('after'),
610
+ 'Failed to activate execution run.',
611
+ )
612
+ const activatedRun = PlanRunSchema.parse(updatedRun)
613
+
614
+ const synced = yield* effectTryPromise(
615
+ () =>
616
+ deps.planExecutor.syncRunGraph({
617
+ tx,
618
+ run: activatedRun,
619
+ spec,
620
+ nodeSpecs,
621
+ nodeRuns,
622
+ artifacts: [],
623
+ emittedBy: params.emittedBy,
624
+ capturedEvents: emittedEvents,
625
+ }),
626
+ 'Failed to sync execution run graph.',
627
+ )
628
+
629
+ yield* emitEvent({
630
+ tx,
631
+ run: synced.run,
632
+ spec,
633
+ eventType: 'plan-approved',
634
+ fromStatus: run.status,
635
+ toStatus: synced.run.status,
636
+ message: `Approved execution plan "${spec.title}".`,
637
+ emittedBy: params.emittedBy,
638
+ detail: { title: spec.title, objective: spec.objective },
639
+ capturedEvents: emittedEvents,
640
+ })
641
+
642
+ const checkpoint = yield* saveCheckpointWithService(deps, {
643
+ tx,
644
+ run: synced.run,
645
+ spec,
646
+ nodeRuns: synced.nodeRuns,
647
+ artifacts: [],
648
+ sequence: nextCheckpointSequence,
649
+ reason: 'plan-approved',
650
+ capturedEvents: emittedEvents,
651
+ })
652
+ yield* attachCheckpoint(tx, synced.run, checkpoint)
653
+ }),
654
+ })
655
+
656
+ yield* effectTryPromise(
657
+ () =>
658
+ attachPlanScheduleIfNeeded({
659
+ db: databaseService,
660
+ planRunService: deps.planRun,
661
+ planSchedulerService: deps.planScheduler,
662
+ organizationId: run.organizationId,
663
+ threadId: run.threadId,
664
+ runId: run.id,
665
+ planSpecId: run.planSpecId,
666
+ }),
667
+ 'Failed to attach execution plan schedule.',
668
+ )
669
+
670
+ return yield* finalizePlanSnapshotEffect(deps, { runId: run.id, emittedBy: params.emittedBy })
671
+ })
672
+ }
673
+
674
+ function rejectPlanEffect(
675
+ deps: ExecutionPlanDeps,
676
+ params: {
677
+ organizationId: RecordIdInput
678
+ threadId: RecordIdInput
679
+ runId: RecordIdInput
680
+ emittedBy: string
681
+ reason?: string
682
+ resolution?: 'rejected' | 'changes-requested'
683
+ },
684
+ ) {
685
+ return Effect.gen(function* () {
686
+ const databaseService = deps.db
687
+ const resolution = params.resolution ?? 'rejected'
688
+ const eventType = resolution === 'changes-requested' ? 'plan-changes-requested' : 'plan-rejected'
689
+ const run = yield* deps.planRun.getRunById(params.runId)
690
+ if (
691
+ recordIdToString(run.organizationId, TABLES.ORGANIZATION) !==
692
+ recordIdToString(params.organizationId, TABLES.ORGANIZATION)
693
+ ) {
694
+ return yield* new BadRequestError({ message: 'Plan run belongs to a different organization.' })
695
+ }
696
+ if (!isPlanVisibleInThreadContext(params.threadId, run)) {
697
+ return yield* new BadRequestError({ message: 'Plan run is not available in this thread context.' })
698
+ }
699
+ if (run.status !== 'pending-approval') {
700
+ return yield* new BadRequestError({ message: 'Only pending-approval plans can be rejected.' })
701
+ }
702
+
703
+ const [spec, nodeRuns, nextCheckpointSequence] = yield* Effect.all([
704
+ deps.planRun.getPlanSpecById(run.planSpecId),
705
+ deps.planRun.listNodeRuns(run.id),
706
+ deps.planRun.getNextCheckpointSequence(run.id),
707
+ ])
708
+
709
+ const checkpointReason = resolution === 'changes-requested' ? 'plan-changes-requested' : 'plan-rejected'
710
+
711
+ yield* withTransactionAndEventsEffect({
712
+ db: databaseService,
713
+ planEventDeliveryService: deps.planEventDelivery,
714
+ run: (tx, emittedEvents) =>
715
+ Effect.gen(function* () {
716
+ const updatedRun = yield* effectTryPromise(
717
+ () =>
718
+ tx
719
+ .update(ensureRecordId(run.id, TABLES.PLAN_RUN))
720
+ .content(
721
+ toRunData(run, {
722
+ status: 'aborted',
723
+ currentNodeId: null,
724
+ waitingNodeId: null,
725
+ readyNodeIds: [],
726
+ completedAt: nowDate(),
727
+ }),
728
+ )
729
+ .output('after'),
730
+ 'Failed to abort execution run.',
731
+ )
732
+ const rejectedRun = PlanRunSchema.parse(updatedRun)
733
+
734
+ yield* emitEvent({
735
+ tx,
736
+ run: rejectedRun,
737
+ spec,
738
+ eventType,
739
+ fromStatus: run.status,
740
+ toStatus: rejectedRun.status,
741
+ message:
742
+ resolution === 'changes-requested'
743
+ ? `Requested changes for execution plan "${spec.title}".`
744
+ : `Rejected execution plan "${spec.title}".`,
745
+ emittedBy: params.emittedBy,
746
+ detail: { resolution, ...(params.reason ? { reason: params.reason } : {}) },
747
+ capturedEvents: emittedEvents,
748
+ })
749
+
750
+ const checkpoint = yield* saveCheckpointWithService(deps, {
751
+ tx,
752
+ run: rejectedRun,
753
+ spec,
754
+ nodeRuns,
755
+ artifacts: [],
756
+ sequence: nextCheckpointSequence,
757
+ reason: checkpointReason,
758
+ capturedEvents: emittedEvents,
759
+ })
760
+ yield* attachCheckpoint(tx, rejectedRun, checkpoint)
761
+ }),
762
+ })
763
+
764
+ const latestRun = yield* deps.planRun.getRunById(run.id)
765
+ return yield* serializeRunFull(deps.planRun, latestRun)
766
+ })
767
+ }
768
+
769
+ function respondToApprovalEffect(
770
+ deps: ExecutionPlanDeps,
771
+ params: {
772
+ threadId: RecordIdInput
773
+ emittedBy: string
774
+ input: { approvalId: string; response: HumanNodeResponsePayload; approvalMessageId?: string }
775
+ },
776
+ ) {
777
+ return Effect.gen(function* () {
778
+ const run = yield* deps.planRun.getActiveRunRecord(params.threadId)
779
+ if (!run) return null
780
+
781
+ const plan = yield* Effect.tryPromise({
782
+ try: () =>
783
+ deps.planExecutor.submitHumanNodeResponse({
784
+ threadId: params.threadId,
785
+ approvalId: params.input.approvalId,
786
+ respondedBy: params.emittedBy,
787
+ response: params.input.response,
788
+ approvalMessageId: params.input.approvalMessageId,
789
+ }),
790
+ catch: (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit human approval response.', cause }),
791
+ })
792
+ if (!plan) return null
793
+
794
+ return yield* finalizePlanSnapshotEffect(deps, { runId: run.id, emittedBy: params.emittedBy })
795
+ })
796
+ }
797
+
798
+ function applyHumanInputFromUserMessageEffect(
799
+ deps: ExecutionPlanDeps,
800
+ params: { threadId: RecordIdInput; message: ChatMessage; respondedBy: string },
801
+ ) {
802
+ return Effect.gen(function* () {
803
+ const run = yield* deps.planRun.getActiveRunRecord(params.threadId)
804
+ if (!run || run.status !== 'awaiting-human' || !run.waitingNodeId) return null
805
+
806
+ const waitingNodeId = run.waitingNodeId
807
+ const nodeSpec = yield* deps.planRun.getNodeSpecByNodeId(run.planSpecId, waitingNodeId)
808
+ const response: HumanNodeResponsePayload | null = Match.value(nodeSpec.type).pipe(
809
+ Match.when('human-input', () => ({
810
+ responseText: extractMessageText(params.message).trim(),
811
+ messageId: params.message.id,
812
+ approved: undefined,
813
+ })),
814
+ Match.when('human-review-edit', () => ({
815
+ responseText: extractMessageText(params.message).trim(),
816
+ messageId: params.message.id,
817
+ approved: undefined,
818
+ })),
819
+ Match.when('human-decision', () => ({
820
+ responseText: extractMessageText(params.message).trim(),
821
+ messageId: params.message.id,
822
+ approved: true as const,
823
+ })),
824
+ Match.orElse(() => null),
825
+ )
826
+ if (!response) return null
827
+
828
+ const plan = yield* Effect.tryPromise({
829
+ try: () =>
830
+ deps.planExecutor.submitHumanNodeResponse({
831
+ threadId: params.threadId,
832
+ respondedBy: params.respondedBy,
833
+ response,
834
+ approvalMessageId: params.message.id,
835
+ }),
836
+ catch: (cause) => new ExecutionPlanServiceError({ message: 'Failed to submit human node response.', cause }),
837
+ })
838
+ if (!plan) return null
839
+
840
+ return yield* finalizePlanSnapshotEffect(deps, { runId: run.id, emittedBy: params.respondedBy })
841
+ })
842
+ }
843
+
844
+ function applyApprovalResponseFromMessagesEffect(
845
+ deps: ExecutionPlanDeps,
846
+ params: { threadId: RecordIdInput; approvalMessages: ChatMessage[]; respondedBy: string },
847
+ ) {
848
+ const approvalResponse = buildApprovalResponseFromMessages(params.approvalMessages)
849
+ if (!approvalResponse) {
850
+ return Effect.succeed<SerializableExecutionPlan | null>(null)
851
+ }
852
+
853
+ return respondToApprovalEffect(deps, {
854
+ threadId: params.threadId,
855
+ emittedBy: params.respondedBy,
856
+ input: approvalResponse,
857
+ })
858
+ }
859
+
860
+ export function makeExecutionPlanService(deps: ExecutionPlanDeps) {
861
+ return {
862
+ hasActivePlan(threadId: RecordIdInput) {
863
+ return hasActivePlanEffect(deps, threadId)
864
+ },
865
+
866
+ getActivePlanForThread(
867
+ threadId: RecordIdInput,
868
+ options?: Partial<ExecutionPlanQueryArgs> & { runId?: RecordIdInput },
869
+ ) {
870
+ return getActivePlanForThreadEffect(deps, threadId, options)
871
+ },
872
+
873
+ getActivePlansForThread(
874
+ threadId: RecordIdInput,
875
+ options?: Partial<ExecutionPlanQueryArgs> & { runId?: RecordIdInput },
876
+ ) {
877
+ return getActivePlansForThreadEffect(deps, threadId, options)
878
+ },
879
+
880
+ getPlansCreatedInContext(params: {
881
+ organizationId: RecordIdInput
882
+ sourceThreadId?: RecordIdInput
883
+ createdByAgentId?: string
884
+ statuses?: ReadonlyArray<PlanRunRecord['status']>
885
+ includeEvents?: boolean
886
+ includeArtifacts?: boolean
887
+ includeApprovals?: boolean
888
+ includeCheckpoints?: boolean
889
+ includeValidationIssues?: boolean
890
+ }) {
891
+ return getPlansCreatedInContextEffect({ ...params, deps })
892
+ },
893
+
894
+ listActivePlanSummaries(threadId: RecordIdInput) {
895
+ return listActivePlanSummariesEffect(deps, threadId)
896
+ },
897
+
898
+ getActivePlanToolResult(params: {
899
+ threadId: RecordIdInput
900
+ runId?: string
901
+ includeEvents?: boolean
902
+ includeArtifacts?: boolean
903
+ includeApprovals?: boolean
904
+ includeCheckpoints?: boolean
905
+ includeValidationIssues?: boolean
906
+ }) {
907
+ return getActivePlanToolResultEffect(deps, params)
908
+ },
909
+
910
+ createPlan(params: {
911
+ organizationId: RecordIdInput
912
+ threadId: RecordIdInput
913
+ sourceThreadId?: RecordIdInput
914
+ leadAgentId: string
915
+ createdByAgentId?: string
916
+ requireApproval?: boolean
917
+ input: PlanDraft
918
+ }) {
919
+ return createPlanEffect(deps, params)
920
+ },
921
+
922
+ replacePlan(params: {
923
+ threadId: RecordIdInput
924
+ organizationId: RecordIdInput
925
+ leadAgentId: string
926
+ createdByAgentId?: string
927
+ input: PlanDraft & { runId: string; reason: string; requireApproval?: boolean }
928
+ }) {
929
+ return replacePlanEffect(deps, params)
930
+ },
931
+
932
+ submitPlanTurnResult(params: {
933
+ threadId: RecordIdInput
934
+ runId: string
935
+ nodeId: string
936
+ emittedBy: string
937
+ input: SubmitPlanTurnResultArgs
938
+ }) {
939
+ return submitPlanTurnResultEffect(deps, params)
940
+ },
941
+ submitNodeResult(params: { threadId: RecordIdInput; emittedBy: string; input: SubmitExecutionNodeResultArgs }) {
942
+ const { runId, nodeId, ...result } = params.input
943
+ return submitPlanTurnResultEffect(deps, {
944
+ threadId: params.threadId,
945
+ emittedBy: params.emittedBy,
946
+ runId,
947
+ nodeId,
948
+ input: result,
949
+ })
950
+ },
951
+
952
+ resumeRun(params: { threadId: RecordIdInput; emittedBy: string; input: { runId: string } }) {
953
+ return resumeRunEffect(deps, params)
954
+ },
955
+
956
+ approvePlan(params: {
957
+ organizationId: RecordIdInput
958
+ threadId: RecordIdInput
959
+ runId: RecordIdInput
960
+ emittedBy: string
961
+ }) {
962
+ return approvePlanEffect(deps, params)
963
+ },
964
+
965
+ rejectPlan(params: {
966
+ organizationId: RecordIdInput
967
+ threadId: RecordIdInput
968
+ runId: RecordIdInput
969
+ emittedBy: string
970
+ reason?: string
971
+ resolution?: 'rejected' | 'changes-requested'
972
+ }) {
973
+ return rejectPlanEffect(deps, params)
974
+ },
975
+
976
+ applyApprovalResponseFromMessages(params: {
977
+ threadId: RecordIdInput
978
+ approvalMessages: ChatMessage[]
979
+ respondedBy: string
980
+ }) {
981
+ return applyApprovalResponseFromMessagesEffect(deps, params)
982
+ },
983
+
984
+ respondToApproval(params: {
985
+ threadId: RecordIdInput
986
+ emittedBy: string
987
+ input: { approvalId: string; response: HumanNodeResponsePayload; approvalMessageId?: string }
988
+ }) {
989
+ return respondToApprovalEffect(deps, params)
990
+ },
991
+
992
+ applyHumanInputFromUserMessage(params: { threadId: RecordIdInput; message: ChatMessage; respondedBy: string }) {
993
+ return applyHumanInputFromUserMessageEffect(deps, params)
994
+ },
995
+
996
+ serializeRuns(runs: PlanRunRecord[], options?: Partial<ExecutionPlanQueryArgs>) {
997
+ return serializeRunsEffect(deps, runs, options)
998
+ },
999
+
1000
+ assertDispatchExecutors(preparedDraft: PreparedPlanDraft) {
1001
+ return assertDispatchExecutorsEffect(deps, preparedDraft)
1002
+ },
1003
+
1004
+ finalizePlanSnapshot(params: { runId: RecordIdInput; emittedBy: string }) {
1005
+ return finalizePlanSnapshotEffect(deps, params)
1006
+ },
1007
+ } as const
1008
+ }
1009
+
1010
+ export class ExecutionPlanServiceTag extends Context.Service<
1011
+ ExecutionPlanServiceTag,
1012
+ ReturnType<typeof makeExecutionPlanService>
1013
+ >()('ExecutionPlanService') {}
1014
+
1015
+ export const ExecutionPlanServiceLive = Layer.effect(
1016
+ ExecutionPlanServiceTag,
1017
+ Effect.gen(function* () {
1018
+ const db = yield* DatabaseServiceTag
1019
+ const planRun = yield* PlanRunServiceTag
1020
+ const planBuilder = yield* PlanBuilderServiceTag
1021
+ const planCheckpoint = yield* PlanCheckpointServiceTag
1022
+ const planCompiler = yield* PlanCompilerServiceTag
1023
+ const planEventDelivery = yield* PlanEventDeliveryServiceTag
1024
+ const planScheduler = yield* PlanSchedulerServiceTag
1025
+ const planValidator = yield* PlanValidatorServiceTag
1026
+ const ownershipDispatcher = yield* OwnershipDispatcherServiceTag
1027
+ const planExecutor = yield* PlanExecutorServiceTag
1028
+ return makeExecutionPlanService({
1029
+ db,
1030
+ ownershipDispatcher,
1031
+ planBuilder,
1032
+ planCheckpoint,
1033
+ planCompiler,
1034
+ planEventDelivery,
1035
+ planExecutor,
1036
+ planRun,
1037
+ planScheduler,
1038
+ planValidator,
1039
+ })
1040
+ }),
1041
+ )