@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
@@ -1,1950 +0,0 @@
1
- import type {
2
- ExecutionPlanToolResultData,
3
- PlanEventRecord,
4
- PlanEventType,
5
- PlanFailureAction,
6
- PlanFailureClass,
7
- PlanNodeResultSubmission,
8
- PlanNodeRunRecord,
9
- PlanNodeSpecRecord,
10
- PlanRunRecord,
11
- PlanSpecRecord,
12
- PlanValidationIssueRecord,
13
- SerializableExecutionPlan,
14
- } from '@lota-sdk/shared'
15
- import {
16
- HUMAN_NODE_TYPES as HUMAN_NODE_TYPE_VALUES,
17
- PlanEventSchema,
18
- PlanNodeAttemptSchema,
19
- PlanNodeRunSchema,
20
- PlanRunSchema,
21
- STRUCTURAL_NODE_TYPES as STRUCTURAL_NODE_TYPE_VALUES,
22
- PlanValidationIssueSchema,
23
- } from '@lota-sdk/shared'
24
- import { RecordId, StringRecordId } from 'surrealdb'
25
-
26
- import { aiLogger } from '../config/logger'
27
- import type { RecordIdInput } from '../db/record-id'
28
- import { ensureRecordId, recordIdToString } from '../db/record-id'
29
- import { databaseService } from '../db/service'
30
- import type { DatabaseTransaction } from '../db/service'
31
- import { TABLES } from '../db/tables'
32
- import { generatedDocumentStorageService } from '../storage/generated-document-storage.service'
33
- import { toDatabaseDateTime } from '../utils/date-time'
34
- import { isRecord } from '../utils/string'
35
- import { artifactService } from './artifact.service'
36
- import { planApprovalService } from './plan-approval.service'
37
- import { planArtifactService } from './plan-artifact.service'
38
- import { planCheckpointService } from './plan-checkpoint.service'
39
- import { runPlanCompletionSideEffectsSafely, runPlanNodeCompletionSideEffects } from './plan-completion-side-effects'
40
- import { planCoordinationService } from './plan-coordination.service'
41
- import { planEventDeliveryService } from './plan-event-delivery.service'
42
- import { isExecutableConditionExpression, readPathValue } from './plan-helpers'
43
- import { toPlanNodeValidationSpec } from './plan-node-spec'
44
- import { buildExecutionPlanToolResult, toRunData } from './plan-run-data'
45
- import type { PlanRunUpdate } from './plan-run-data'
46
- import { planRunService } from './plan-run.service'
47
- import { planSchedulerService } from './plan-scheduler.service'
48
- import type { PlanValidationIssueInput } from './plan-validator.service'
49
- import { planValidatorService } from './plan-validator.service'
50
-
51
- const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped', 'scheduled', 'monitoring'])
52
- const HUMAN_NODE_TYPE_SET = new Set<string>(HUMAN_NODE_TYPE_VALUES)
53
- const STRUCTURAL_NODE_TYPE_SET = new Set<string>(STRUCTURAL_NODE_TYPE_VALUES)
54
-
55
- function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
56
- const segments = path
57
- .split('.')
58
- .map((part) => part.trim())
59
- .filter(Boolean)
60
- if (segments.length === 0) return
61
- const lastSegment = segments.at(-1)
62
- if (!lastSegment) return
63
-
64
- let current: Record<string, unknown> = target
65
- for (const segment of segments.slice(0, -1)) {
66
- const next = current[segment]
67
- if (!isRecord(next)) {
68
- current[segment] = {}
69
- }
70
- current = current[segment] as Record<string, unknown>
71
- }
72
- current[lastSegment] = value
73
- }
74
-
75
- function parseLiteralValue(raw: string): unknown {
76
- const trimmed = raw.trim()
77
- if (!trimmed.length) return undefined
78
- if (trimmed === 'true') return true
79
- if (trimmed === 'false') return false
80
- if (trimmed === 'null') return null
81
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)
82
- if (
83
- (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
84
- (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
85
- (trimmed.startsWith('`') && trimmed.endsWith('`'))
86
- ) {
87
- return trimmed.slice(1, -1)
88
- }
89
-
90
- try {
91
- return JSON.parse(trimmed)
92
- } catch {
93
- return trimmed
94
- }
95
- }
96
-
97
- function buildArtifactContext(artifacts: Array<{ name: string; kind: string; payload?: unknown }>) {
98
- return Object.fromEntries(
99
- artifacts.map((artifact) => [artifact.name, { kind: artifact.kind, payload: artifact.payload }]),
100
- )
101
- }
102
-
103
- function buildNodeContext(params: {
104
- nodeRun: PlanNodeRunRecord | undefined
105
- artifacts: Array<{ name: string; kind: string; payload?: unknown }>
106
- }) {
107
- return {
108
- input: params.nodeRun?.resolvedInput ?? {},
109
- output: params.nodeRun?.latestStructuredOutput ?? {},
110
- artifact: buildArtifactContext(params.artifacts),
111
- }
112
- }
113
-
114
- function buildPublishedArtifactContent(params: {
115
- artifact: PlanNodeResultSubmission['artifacts'][number]
116
- notes: string
117
- }): string {
118
- const { artifact, notes } = params
119
- if (artifact.content?.trim()) {
120
- return artifact.content
121
- }
122
-
123
- if (artifact.payload !== undefined) {
124
- return `# ${artifact.name}\n\n\`\`\`json\n${JSON.stringify(artifact.payload, null, 2)}\n\`\`\``
125
- }
126
-
127
- return `# ${artifact.name}\n\n${notes.trim()}`
128
- }
129
-
130
- function evaluateCondition(expression: string | undefined, context: Record<string, unknown>): boolean {
131
- if (!expression?.trim()) return true
132
- const normalized = expression.trim()
133
- if (normalized === 'always') return true
134
-
135
- const match = normalized.match(/^([a-zA-Z0-9_.]+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/)
136
- if (!match) {
137
- if (!isExecutableConditionExpression(normalized)) {
138
- return true
139
- }
140
- return Boolean(readPathValue(context, normalized))
141
- }
142
-
143
- const [, leftPath, operator, rawRightValue] = match
144
- const left = readPathValue(context, leftPath)
145
- const right = parseLiteralValue(rawRightValue)
146
-
147
- if (operator === '==') return Object.is(left, right)
148
- if (operator === '!=') return !Object.is(left, right)
149
- if (typeof left !== 'number' || typeof right !== 'number') return false
150
- if (operator === '>=') return left >= right
151
- if (operator === '<=') return left <= right
152
- if (operator === '>') return left > right
153
- return left < right
154
- }
155
-
156
- function isSuccessfulTerminalStatus(status: string): boolean {
157
- return SUCCESSFUL_TERMINAL_NODE_STATUSES.has(status)
158
- }
159
-
160
- function isHumanNodeType(type: string): boolean {
161
- return HUMAN_NODE_TYPE_SET.has(type)
162
- }
163
-
164
- function isStructuralNodeType(type: string): boolean {
165
- return STRUCTURAL_NODE_TYPE_SET.has(type)
166
- }
167
-
168
- type PlanNodeRunUpdate = Omit<
169
- Partial<PlanNodeRunRecord>,
170
- | 'blockedReason'
171
- | 'failureClass'
172
- | 'resolvedInput'
173
- | 'latestStructuredOutput'
174
- | 'latestNotes'
175
- | 'latestAttemptId'
176
- | 'scheduledAt'
177
- | 'readyAt'
178
- | 'startedAt'
179
- | 'completedAt'
180
- > & {
181
- blockedReason?: string | null
182
- failureClass?: PlanFailureClass | null
183
- resolvedInput?: Record<string, unknown> | null
184
- latestStructuredOutput?: Record<string, unknown> | null
185
- latestNotes?: string | null
186
- latestAttemptId?: RecordIdInput | null
187
- scheduledAt?: string | Date | null
188
- readyAt?: string | Date | null
189
- startedAt?: string | Date | null
190
- completedAt?: string | Date | null
191
- }
192
-
193
- function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
194
- return {
195
- runId: ensureRecordId(nodeRun.runId, TABLES.PLAN_RUN),
196
- planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC),
197
- nodeId: nodeRun.nodeId,
198
- status: patch.status ?? nodeRun.status,
199
- attemptCount: patch.attemptCount ?? nodeRun.attemptCount,
200
- retryCount: patch.retryCount ?? nodeRun.retryCount,
201
- ...(patch.resolvedInput === null
202
- ? {}
203
- : patch.resolvedInput !== undefined
204
- ? { resolvedInput: patch.resolvedInput }
205
- : nodeRun.resolvedInput
206
- ? { resolvedInput: nodeRun.resolvedInput }
207
- : {}),
208
- ...(patch.latestStructuredOutput === null
209
- ? {}
210
- : patch.latestStructuredOutput !== undefined
211
- ? { latestStructuredOutput: patch.latestStructuredOutput }
212
- : nodeRun.latestStructuredOutput
213
- ? { latestStructuredOutput: nodeRun.latestStructuredOutput }
214
- : {}),
215
- ...(patch.latestNotes === null
216
- ? {}
217
- : patch.latestNotes !== undefined
218
- ? { latestNotes: patch.latestNotes }
219
- : nodeRun.latestNotes
220
- ? { latestNotes: nodeRun.latestNotes }
221
- : {}),
222
- ...(patch.latestAttemptId === null
223
- ? {}
224
- : patch.latestAttemptId !== undefined
225
- ? { latestAttemptId: ensureRecordId(patch.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
226
- : nodeRun.latestAttemptId
227
- ? { latestAttemptId: ensureRecordId(nodeRun.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
228
- : {}),
229
- ...(patch.blockedReason === null
230
- ? {}
231
- : patch.blockedReason !== undefined
232
- ? { blockedReason: patch.blockedReason }
233
- : nodeRun.blockedReason
234
- ? { blockedReason: nodeRun.blockedReason }
235
- : {}),
236
- ...(patch.failureClass === null
237
- ? {}
238
- : patch.failureClass !== undefined
239
- ? { failureClass: patch.failureClass }
240
- : nodeRun.failureClass
241
- ? { failureClass: nodeRun.failureClass }
242
- : {}),
243
- ...(patch.scheduledAt === null
244
- ? {}
245
- : patch.scheduledAt !== undefined
246
- ? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
247
- : nodeRun.scheduledAt
248
- ? { scheduledAt: toDatabaseDateTime(nodeRun.scheduledAt) }
249
- : {}),
250
- ...(patch.readyAt === null
251
- ? {}
252
- : patch.readyAt !== undefined
253
- ? { readyAt: toDatabaseDateTime(patch.readyAt) }
254
- : nodeRun.readyAt
255
- ? { readyAt: toDatabaseDateTime(nodeRun.readyAt) }
256
- : {}),
257
- ...(patch.startedAt === null
258
- ? {}
259
- : patch.startedAt !== undefined
260
- ? { startedAt: toDatabaseDateTime(patch.startedAt) }
261
- : nodeRun.startedAt
262
- ? { startedAt: toDatabaseDateTime(nodeRun.startedAt) }
263
- : {}),
264
- ...(patch.completedAt === null
265
- ? {}
266
- : patch.completedAt !== undefined
267
- ? { completedAt: toDatabaseDateTime(patch.completedAt) }
268
- : nodeRun.completedAt
269
- ? { completedAt: toDatabaseDateTime(nodeRun.completedAt) }
270
- : {}),
271
- }
272
- }
273
-
274
- function deriveApprovalStatus(response: Record<string, unknown>): 'approved' | 'rejected' | 'changes-requested' {
275
- const approved = response.approved === true
276
- const requiredEdits = Array.isArray(response.requiredEdits)
277
- ? response.requiredEdits.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
278
- : []
279
-
280
- if (approved && requiredEdits.length === 0) return 'approved'
281
- if (requiredEdits.length > 0) return 'changes-requested'
282
- return 'rejected'
283
- }
284
-
285
- class PlanExecutorService {
286
- async submitNodeResult(params: {
287
- threadId: RecordIdInput
288
- runId: string
289
- nodeId: string
290
- emittedBy: string
291
- result: PlanNodeResultSubmission
292
- }): Promise<ExecutionPlanToolResultData> {
293
- const run = await planRunService.getRunById(params.runId)
294
- if (recordIdToString(run.threadId, TABLES.THREAD) !== recordIdToString(params.threadId, TABLES.THREAD)) {
295
- throw new Error('Execution node result targets a different thread.')
296
- }
297
- if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
298
- throw new Error('Execution run is no longer active.')
299
- }
300
-
301
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
302
- const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
303
- const nodeSpec = nodeSpecs.find((candidate) => candidate.nodeId === params.nodeId)
304
- if (!nodeSpec) {
305
- throw new Error(`Execution node "${params.nodeId}" does not exist in this run.`)
306
- }
307
- if (isHumanNodeType(nodeSpec.type) || isStructuralNodeType(nodeSpec.type)) {
308
- throw new Error(
309
- `Execution node "${nodeSpec.label}" is executor-owned and cannot accept direct result submission.`,
310
- )
311
- }
312
-
313
- const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
314
- if (nodeRun.status !== 'running') {
315
- throw new Error(`Execution node "${nodeSpec.label}" is not currently running.`)
316
- }
317
-
318
- const existingArtifacts = await planRunService.listArtifacts(run.id)
319
- const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
320
- const validation = planValidatorService.validateNodeResult({
321
- draft: { schemas: spec.schemaRegistry },
322
- node: toPlanNodeValidationSpec(nodeSpec),
323
- result: params.result,
324
- })
325
- const emittedEvents: PlanEventRecord[] = []
326
- const publishedArtifactStorageKeys: string[] = []
327
-
328
- try {
329
- await databaseService.withTransaction(async (tx) => {
330
- const attempt = await this.createAttempt({
331
- tx,
332
- run,
333
- nodeRun,
334
- emittedBy: params.emittedBy,
335
- result: params.result,
336
- status: validation.blocking.length > 0 ? 'failed' : 'completed',
337
- failureClass: validation.failureClass,
338
- })
339
-
340
- const issues = await this.persistValidationIssues({
341
- tx,
342
- run,
343
- spec,
344
- attemptId: attempt.id,
345
- nodeId: params.nodeId,
346
- issues: [...validation.blocking, ...validation.warnings],
347
- })
348
-
349
- const finalizedAttempt = PlanNodeAttemptSchema.parse(
350
- await tx
351
- .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
352
- .content({
353
- runId: ensureRecordId(attempt.runId, TABLES.PLAN_RUN),
354
- nodeRunId: ensureRecordId(attempt.nodeRunId, TABLES.PLAN_NODE_RUN),
355
- nodeId: attempt.nodeId,
356
- emittedBy: attempt.emittedBy,
357
- status: attempt.status,
358
- ...(attempt.structuredOutput ? { structuredOutput: attempt.structuredOutput } : {}),
359
- ...(attempt.notes ? { notes: attempt.notes } : {}),
360
- validationIssueIds: issues.map((issue) => ensureRecordId(issue.id, TABLES.PLAN_VALIDATION_ISSUE)),
361
- ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
362
- })
363
- .output('after'),
364
- )
365
-
366
- const publishedArtifacts =
367
- validation.blocking.length > 0
368
- ? params.result.artifacts
369
- : await Promise.all(
370
- params.result.artifacts.map(async (artifact) => {
371
- const deliverable = nodeSpec.deliverables.find((candidate) => candidate.name === artifact.name)
372
- if (!deliverable?.publishAs) {
373
- return artifact
374
- }
375
-
376
- const content = buildPublishedArtifactContent({ artifact, notes: params.result.notes })
377
- const published = await artifactService.publishArtifactInTransaction(
378
- {
379
- organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
380
- authorAgentId: params.emittedBy,
381
- title: artifact.name,
382
- artifactKind: deliverable.publishAs.artifactKind,
383
- templateId: deliverable.publishAs.templateId,
384
- canonicalKey: deliverable.publishAs.canonicalKey,
385
- content,
386
- tags: [],
387
- ...(artifact.description ? { description: artifact.description } : {}),
388
- sourceThreadId: recordIdToString(run.threadId, TABLES.THREAD),
389
- sourcePlanRunId: recordIdToString(run.id, TABLES.PLAN_RUN),
390
- sourcePlanNodeId: params.nodeId,
391
- deliverableName: artifact.name,
392
- },
393
- tx,
394
- )
395
- publishedArtifactStorageKeys.push(published.storageKey)
396
-
397
- return { ...artifact, content, publishedArtifactId: recordIdToString(published.id, TABLES.ARTIFACT) }
398
- }),
399
- )
400
-
401
- const persistedArtifacts = await planArtifactService.persistArtifacts({
402
- tx,
403
- runId: run.id,
404
- attemptId: finalizedAttempt.id,
405
- nodeId: params.nodeId,
406
- artifacts: publishedArtifacts,
407
- })
408
-
409
- let nextNodeRun = PlanNodeRunSchema.parse(
410
- await tx
411
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
412
- .content(
413
- toNodeRunData(nodeRun, {
414
- attemptCount: nodeRun.attemptCount + 1,
415
- latestAttemptId: finalizedAttempt.id,
416
- latestStructuredOutput: params.result.structuredOutput ?? null,
417
- latestNotes: params.result.notes,
418
- }),
419
- )
420
- .output('after'),
421
- )
422
-
423
- const nodeRuns = await planRunService.listNodeRuns(run.id)
424
- const withUpdatedNodeRuns = nodeRuns.map((candidate) =>
425
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
426
- )
427
- const nextArtifacts = [...existingArtifacts, ...persistedArtifacts]
428
-
429
- if (validation.blocking.length > 0) {
430
- const shouldRetry =
431
- validation.failureClass &&
432
- nodeSpec.retryPolicy.maxAttempts > nextNodeRun.retryCount &&
433
- (nodeSpec.retryPolicy.retryOn.length === 0 ||
434
- nodeSpec.retryPolicy.retryOn.includes(validation.failureClass))
435
-
436
- if (shouldRetry) {
437
- nextNodeRun = PlanNodeRunSchema.parse(
438
- await tx
439
- .update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
440
- .content(
441
- toNodeRunData(nextNodeRun, {
442
- status: 'ready',
443
- retryCount: nextNodeRun.retryCount + 1,
444
- failureClass: validation.failureClass,
445
- blockedReason: validation.blocking[0]?.message ?? null,
446
- readyAt: new Date(),
447
- startedAt: null,
448
- completedAt: null,
449
- }),
450
- )
451
- .output('after'),
452
- )
453
-
454
- await this.emitEvent({
455
- tx,
456
- run,
457
- spec,
458
- nodeId: nextNodeRun.nodeId,
459
- attemptId: finalizedAttempt.id,
460
- eventType: 'validation-reported',
461
- message: `Validation failed for node "${nodeSpec.label}", scheduling retry.`,
462
- detail: { issues: validation.blocking.map((issue) => issue.code) },
463
- emittedBy: params.emittedBy,
464
- capturedEvents: emittedEvents,
465
- })
466
- await this.emitEvent({
467
- tx,
468
- run,
469
- spec,
470
- nodeId: nextNodeRun.nodeId,
471
- attemptId: finalizedAttempt.id,
472
- eventType: 'node-unblocked',
473
- fromStatus: nodeRun.status,
474
- toStatus: nextNodeRun.status,
475
- message: `Node "${nodeSpec.label}" is ready for another attempt.`,
476
- detail: { retryCount: nextNodeRun.retryCount },
477
- emittedBy: params.emittedBy,
478
- capturedEvents: emittedEvents,
479
- })
480
-
481
- const synced = await this.syncRunGraph({
482
- tx,
483
- run,
484
- spec,
485
- nodeSpecs,
486
- nodeRuns: withUpdatedNodeRuns.map((candidate) =>
487
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
488
- ),
489
- artifacts: nextArtifacts,
490
- emittedBy: params.emittedBy,
491
- capturedEvents: emittedEvents,
492
- })
493
-
494
- const checkpoint = await this.saveCheckpoint({
495
- tx,
496
- run: synced.run,
497
- spec,
498
- nodeRuns: synced.nodeRuns,
499
- artifacts: synced.artifacts,
500
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
501
- reason: 'node-result-retry',
502
- capturedEvents: emittedEvents,
503
- })
504
-
505
- await this.attachCheckpoint(tx, synced.run, checkpoint)
506
- return
507
- }
508
-
509
- const failureAction = this.resolveFailureAction(nodeSpec, validation.failureClass)
510
- if (failureAction === 'human-review') {
511
- nextNodeRun = PlanNodeRunSchema.parse(
512
- await tx
513
- .update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
514
- .content(
515
- toNodeRunData(nextNodeRun, {
516
- status: 'awaiting-human',
517
- retryCount: nextNodeRun.retryCount + 1,
518
- failureClass: validation.failureClass,
519
- blockedReason: validation.blocking[0]?.message ?? null,
520
- startedAt: nextNodeRun.startedAt ?? new Date(),
521
- }),
522
- )
523
- .output('after'),
524
- )
525
-
526
- const approval = await planApprovalService.createPendingApproval({
527
- tx,
528
- runId: run.id,
529
- nodeRunId: nextNodeRun.id,
530
- nodeId: nextNodeRun.nodeId,
531
- requestedBy: params.emittedBy,
532
- presented: {
533
- nodeId: nodeSpec.nodeId,
534
- label: nodeSpec.label,
535
- objective: nodeSpec.objective,
536
- instructions: nodeSpec.instructions,
537
- validationIssues: validation.blocking,
538
- },
539
- })
540
-
541
- const failedRun = await this.replaceRun(tx, run, {
542
- status: 'awaiting-human',
543
- currentNodeId: nextNodeRun.nodeId,
544
- waitingNodeId: nextNodeRun.nodeId,
545
- readyNodeIds: [],
546
- failureCount: run.failureCount + 1,
547
- })
548
-
549
- await this.emitEvent({
550
- tx,
551
- run: failedRun,
552
- spec,
553
- nodeId: nextNodeRun.nodeId,
554
- attemptId: finalizedAttempt.id,
555
- approvalId: approval.id,
556
- eventType: 'approval-requested',
557
- fromStatus: run.status,
558
- toStatus: failedRun.status,
559
- message: `Node "${nodeSpec.label}" requires human review before continuing.`,
560
- detail: { issues: validation.blocking.map((issue) => issue.code) },
561
- emittedBy: params.emittedBy,
562
- capturedEvents: emittedEvents,
563
- })
564
-
565
- const checkpoint = await this.saveCheckpoint({
566
- tx,
567
- run: failedRun,
568
- spec,
569
- nodeRuns: withUpdatedNodeRuns.map((candidate) =>
570
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
571
- ),
572
- artifacts: nextArtifacts,
573
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
574
- reason: 'node-result-human-review',
575
- capturedEvents: emittedEvents,
576
- })
577
-
578
- await this.attachCheckpoint(tx, failedRun, checkpoint)
579
- return
580
- }
581
-
582
- if (failureAction === 'replan') {
583
- nextNodeRun = PlanNodeRunSchema.parse(
584
- await tx
585
- .update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
586
- .content(
587
- toNodeRunData(nextNodeRun, {
588
- status: 'blocked',
589
- retryCount: nextNodeRun.retryCount + 1,
590
- failureClass: validation.failureClass,
591
- blockedReason: validation.blocking[0]?.message ?? null,
592
- }),
593
- )
594
- .output('after'),
595
- )
596
-
597
- const blockedRun = await this.replaceRun(tx, run, {
598
- status: 'blocked',
599
- currentNodeId: nextNodeRun.nodeId,
600
- waitingNodeId: null,
601
- readyNodeIds: [],
602
- failureCount: run.failureCount + 1,
603
- })
604
-
605
- await this.emitEvent({
606
- tx,
607
- run: blockedRun,
608
- spec,
609
- nodeId: nextNodeRun.nodeId,
610
- attemptId: finalizedAttempt.id,
611
- eventType: 'node-blocked',
612
- fromStatus: nodeRun.status,
613
- toStatus: nextNodeRun.status,
614
- message: `Node "${nodeSpec.label}" failed validation and requires replanning.`,
615
- detail: { failureClass: validation.failureClass, issues: validation.blocking.map((issue) => issue.code) },
616
- emittedBy: params.emittedBy,
617
- capturedEvents: emittedEvents,
618
- })
619
-
620
- const checkpoint = await this.saveCheckpoint({
621
- tx,
622
- run: blockedRun,
623
- spec,
624
- nodeRuns: withUpdatedNodeRuns.map((candidate) =>
625
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
626
- ),
627
- artifacts: nextArtifacts,
628
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
629
- reason: 'node-result-replan',
630
- capturedEvents: emittedEvents,
631
- })
632
-
633
- await this.attachCheckpoint(tx, blockedRun, checkpoint)
634
- return
635
- }
636
-
637
- nextNodeRun = PlanNodeRunSchema.parse(
638
- await tx
639
- .update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
640
- .content(
641
- toNodeRunData(nextNodeRun, {
642
- status: 'failed',
643
- retryCount: nextNodeRun.retryCount + 1,
644
- failureClass: validation.failureClass,
645
- blockedReason: validation.blocking[0]?.message ?? null,
646
- completedAt: new Date(),
647
- }),
648
- )
649
- .output('after'),
650
- )
651
-
652
- const failedRun = await this.replaceRun(tx, run, {
653
- status: 'failed',
654
- currentNodeId: null,
655
- waitingNodeId: null,
656
- readyNodeIds: [],
657
- failureCount: run.failureCount + 1,
658
- completedAt: new Date(),
659
- })
660
-
661
- await this.emitEvent({
662
- tx,
663
- run: failedRun,
664
- spec,
665
- nodeId: nextNodeRun.nodeId,
666
- attemptId: finalizedAttempt.id,
667
- eventType: 'node-failed',
668
- fromStatus: nodeRun.status,
669
- toStatus: nextNodeRun.status,
670
- message: `Node "${nodeSpec.label}" failed validation and the run has been aborted.`,
671
- detail: { failureClass: validation.failureClass, issues: validation.blocking.map((issue) => issue.code) },
672
- emittedBy: params.emittedBy,
673
- capturedEvents: emittedEvents,
674
- })
675
-
676
- const checkpoint = await this.saveCheckpoint({
677
- tx,
678
- run: failedRun,
679
- spec,
680
- nodeRuns: withUpdatedNodeRuns.map((candidate) =>
681
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
682
- ),
683
- artifacts: nextArtifacts,
684
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
685
- reason: 'node-result-failed',
686
- capturedEvents: emittedEvents,
687
- })
688
-
689
- await this.attachCheckpoint(tx, failedRun, checkpoint)
690
- return
691
- }
692
-
693
- nextNodeRun = PlanNodeRunSchema.parse(
694
- await tx
695
- .update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
696
- .content(
697
- toNodeRunData(nextNodeRun, {
698
- status: validation.warnings.length > 0 ? 'partial' : 'completed',
699
- latestAttemptId: finalizedAttempt.id,
700
- latestStructuredOutput: params.result.structuredOutput ?? null,
701
- latestNotes: params.result.notes,
702
- blockedReason: null,
703
- failureClass: null,
704
- completedAt: new Date(),
705
- }),
706
- )
707
- .output('after'),
708
- )
709
-
710
- await this.emitEvent({
711
- tx,
712
- run,
713
- spec,
714
- nodeId: nextNodeRun.nodeId,
715
- attemptId: finalizedAttempt.id,
716
- eventType: validation.warnings.length > 0 ? 'node-partial' : 'node-completed',
717
- fromStatus: nodeRun.status,
718
- toStatus: nextNodeRun.status,
719
- message:
720
- validation.warnings.length > 0
721
- ? `Node "${nodeSpec.label}" completed with warnings.`
722
- : `Node "${nodeSpec.label}" completed successfully.`,
723
- detail: { warningCount: validation.warnings.length },
724
- emittedBy: params.emittedBy,
725
- capturedEvents: emittedEvents,
726
- })
727
-
728
- const synced = await this.syncRunGraph({
729
- tx,
730
- run,
731
- spec,
732
- nodeSpecs,
733
- nodeRuns: withUpdatedNodeRuns.map((candidate) =>
734
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
735
- ),
736
- artifacts: nextArtifacts,
737
- emittedBy: params.emittedBy,
738
- capturedEvents: emittedEvents,
739
- })
740
-
741
- const checkpoint = await this.saveCheckpoint({
742
- tx,
743
- run: synced.run,
744
- spec,
745
- nodeRuns: synced.nodeRuns,
746
- artifacts: synced.artifacts,
747
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
748
- reason: 'node-result-complete',
749
- capturedEvents: emittedEvents,
750
- })
751
-
752
- await this.attachCheckpoint(tx, synced.run, checkpoint)
753
- })
754
- } catch (error) {
755
- await Promise.allSettled(
756
- publishedArtifactStorageKeys.map((storageKey) =>
757
- generatedDocumentStorageService.deleteTextArtifact(storageKey),
758
- ),
759
- )
760
- throw error
761
- }
762
-
763
- const orgId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
764
- const runIdStr = recordIdToString(run.id, TABLES.PLAN_RUN)
765
- void runPlanNodeCompletionSideEffects({
766
- runId: runIdStr,
767
- organizationId: orgId,
768
- nodeId: params.nodeId,
769
- nodeLabel: nodeSpec.label,
770
- nodeOwnerRef: nodeSpec.owner.ref,
771
- nodeOwnerType: nodeSpec.owner.executorType,
772
- nodeType: nodeSpec.type,
773
- nodeStartedAt: nodeRun.startedAt,
774
- nodeAttemptCount: nodeRun.attemptCount + 1,
775
- artifactCount: params.result.artifacts.length,
776
- validationIssues: [...validation.blocking, ...validation.warnings],
777
- }).catch((error) => {
778
- aiLogger.warn`Failed to record node completion metrics for run ${runIdStr} node ${params.nodeId}: ${error instanceof Error ? error.message : String(error)}`
779
- })
780
-
781
- const updatedRun = await planRunService.getRunById(run.id)
782
- if (updatedRun.status === 'completed') {
783
- void runPlanCompletionSideEffectsSafely({ runId: runIdStr, organizationId: orgId })
784
- }
785
-
786
- await planEventDeliveryService.dispatchEvents(emittedEvents)
787
-
788
- const snapshot = await planRunService.toSerializablePlan(updatedRun, {
789
- includeEvents: true,
790
- includeArtifacts: true,
791
- includeApprovals: true,
792
- includeCheckpoints: true,
793
- includeValidationIssues: true,
794
- })
795
-
796
- return buildExecutionPlanToolResult({
797
- action: 'node-result-submitted',
798
- plan: snapshot,
799
- message: `Submitted result for node "${nodeSpec.label}".`,
800
- })
801
- }
802
-
803
- async submitHumanNodeResponse(params: {
804
- threadId: RecordIdInput
805
- approvalId?: string
806
- respondedBy: string
807
- response: Record<string, unknown>
808
- approvalMessageId?: string
809
- }): Promise<SerializableExecutionPlan | null> {
810
- const run = await planRunService.getActiveRunRecord(params.threadId)
811
- if (!run || run.status !== 'awaiting-human' || !run.waitingNodeId) {
812
- return null
813
- }
814
-
815
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
816
- const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
817
- const nodeSpec = nodeSpecs.find((candidate) => candidate.nodeId === run.waitingNodeId)
818
- if (!nodeSpec) {
819
- throw new Error(`Waiting node "${run.waitingNodeId}" does not exist.`)
820
- }
821
-
822
- const nodeRun = await planRunService.getNodeRunByNodeId(run.id, run.waitingNodeId)
823
- const approval =
824
- (params.approvalId ? await planApprovalService.getApprovalById(params.approvalId) : null) ??
825
- (await planApprovalService.getPendingApprovalForNodeRun(nodeRun.id))
826
- if (!approval) {
827
- throw new Error(`No pending approval exists for node "${nodeSpec.label}".`)
828
- }
829
-
830
- const existingArtifacts = await planRunService.listArtifacts(run.id)
831
- const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
832
- const validation = planValidatorService.validateNodeResult({
833
- draft: { schemas: spec.schemaRegistry },
834
- node: toPlanNodeValidationSpec(nodeSpec),
835
- result: {
836
- structuredOutput: params.response,
837
- artifacts: [],
838
- notes: typeof params.response.comments === 'string' ? params.response.comments : 'Human response submitted.',
839
- },
840
- })
841
- const emittedEvents: PlanEventRecord[] = []
842
-
843
- await databaseService.withTransaction(async (tx) => {
844
- const approvalStatus = deriveApprovalStatus(params.response)
845
- await planApprovalService.updateApprovalResponse({
846
- tx,
847
- approval,
848
- status: approvalStatus,
849
- response: params.response,
850
- respondedBy: params.respondedBy,
851
- approvalMessageId: params.approvalMessageId,
852
- comments: typeof params.response.comments === 'string' ? params.response.comments : undefined,
853
- requiredEdits: Array.isArray(params.response.requiredEdits)
854
- ? params.response.requiredEdits.filter((entry): entry is string => typeof entry === 'string')
855
- : undefined,
856
- })
857
-
858
- const attempt = await this.createAttempt({
859
- tx,
860
- run,
861
- nodeRun,
862
- emittedBy: params.respondedBy,
863
- result: {
864
- structuredOutput: params.response,
865
- artifacts: [],
866
- notes: typeof params.response.comments === 'string' ? params.response.comments : 'Human response submitted.',
867
- },
868
- status: validation.blocking.length > 0 ? 'failed' : 'completed',
869
- failureClass: validation.failureClass,
870
- })
871
-
872
- const issues = await this.persistValidationIssues({
873
- tx,
874
- run,
875
- spec,
876
- attemptId: attempt.id,
877
- nodeId: nodeRun.nodeId,
878
- issues: [...validation.blocking, ...validation.warnings],
879
- })
880
-
881
- await tx
882
- .update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
883
- .content({
884
- runId: ensureRecordId(attempt.runId, TABLES.PLAN_RUN),
885
- nodeRunId: ensureRecordId(attempt.nodeRunId, TABLES.PLAN_NODE_RUN),
886
- nodeId: attempt.nodeId,
887
- emittedBy: attempt.emittedBy,
888
- status: attempt.status,
889
- structuredOutput: params.response,
890
- validationIssueIds: issues.map((issue) => ensureRecordId(issue.id, TABLES.PLAN_VALIDATION_ISSUE)),
891
- ...(attempt.notes ? { notes: attempt.notes } : {}),
892
- ...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
893
- })
894
- .output('after')
895
-
896
- const nextNodeRun =
897
- validation.blocking.length > 0
898
- ? PlanNodeRunSchema.parse(
899
- await tx
900
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
901
- .content(
902
- toNodeRunData(nodeRun, {
903
- status: 'blocked',
904
- attemptCount: nodeRun.attemptCount + 1,
905
- latestAttemptId: attempt.id,
906
- latestStructuredOutput: params.response,
907
- latestNotes: typeof params.response.comments === 'string' ? params.response.comments : null,
908
- blockedReason: validation.blocking[0]?.message ?? null,
909
- failureClass: validation.failureClass,
910
- }),
911
- )
912
- .output('after'),
913
- )
914
- : PlanNodeRunSchema.parse(
915
- await tx
916
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
917
- .content(
918
- toNodeRunData(nodeRun, {
919
- status: 'completed',
920
- attemptCount: nodeRun.attemptCount + 1,
921
- latestAttemptId: attempt.id,
922
- latestStructuredOutput: params.response,
923
- latestNotes: typeof params.response.comments === 'string' ? params.response.comments : null,
924
- blockedReason: null,
925
- failureClass: null,
926
- completedAt: new Date(),
927
- }),
928
- )
929
- .output('after'),
930
- )
931
-
932
- const nodeRuns = (await planRunService.listNodeRuns(run.id)).map((candidate) =>
933
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
934
- )
935
-
936
- if (validation.blocking.length > 0) {
937
- const blockedRun = await this.replaceRun(tx, run, {
938
- status: 'blocked',
939
- currentNodeId: nextNodeRun.nodeId,
940
- waitingNodeId: null,
941
- readyNodeIds: [],
942
- failureCount: run.failureCount + 1,
943
- })
944
-
945
- await this.emitEvent({
946
- tx,
947
- run: blockedRun,
948
- spec,
949
- nodeId: nextNodeRun.nodeId,
950
- attemptId: attempt.id,
951
- approvalId: approval.id,
952
- eventType: 'approval-resolved',
953
- fromStatus: run.status,
954
- toStatus: blockedRun.status,
955
- message: `Human response for node "${nodeSpec.label}" blocked execution.`,
956
- detail: { issues: validation.blocking.map((issue) => issue.code) },
957
- emittedBy: params.respondedBy,
958
- capturedEvents: emittedEvents,
959
- })
960
-
961
- const checkpoint = await this.saveCheckpoint({
962
- tx,
963
- run: blockedRun,
964
- spec,
965
- nodeRuns,
966
- artifacts: existingArtifacts,
967
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
968
- reason: 'human-response-blocked',
969
- capturedEvents: emittedEvents,
970
- })
971
- await this.attachCheckpoint(tx, blockedRun, checkpoint)
972
- return
973
- }
974
-
975
- const synced = await this.syncRunGraph({
976
- tx,
977
- run,
978
- spec,
979
- nodeSpecs,
980
- nodeRuns,
981
- artifacts: existingArtifacts,
982
- emittedBy: params.respondedBy,
983
- capturedEvents: emittedEvents,
984
- })
985
-
986
- await this.emitEvent({
987
- tx,
988
- run: synced.run,
989
- spec,
990
- nodeId: nextNodeRun.nodeId,
991
- attemptId: attempt.id,
992
- approvalId: approval.id,
993
- eventType: 'approval-resolved',
994
- fromStatus: run.status,
995
- toStatus: synced.run.status,
996
- message: `Human response for node "${nodeSpec.label}" accepted.`,
997
- detail: { approvalStatus: approvalStatus },
998
- emittedBy: params.respondedBy,
999
- capturedEvents: emittedEvents,
1000
- })
1001
-
1002
- const checkpoint = await this.saveCheckpoint({
1003
- tx,
1004
- run: synced.run,
1005
- spec,
1006
- nodeRuns: synced.nodeRuns,
1007
- artifacts: synced.artifacts,
1008
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
1009
- reason: 'human-response-complete',
1010
- capturedEvents: emittedEvents,
1011
- })
1012
- await this.attachCheckpoint(tx, synced.run, checkpoint)
1013
- })
1014
-
1015
- await planEventDeliveryService.dispatchEvents(emittedEvents)
1016
-
1017
- return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
1018
- includeEvents: true,
1019
- includeArtifacts: true,
1020
- includeApprovals: true,
1021
- includeCheckpoints: true,
1022
- includeValidationIssues: true,
1023
- })
1024
- }
1025
-
1026
- async resumeRun(params: {
1027
- threadId: RecordIdInput
1028
- runId: string
1029
- emittedBy: string
1030
- }): Promise<ExecutionPlanToolResultData> {
1031
- const run = await planRunService.getRunById(params.runId)
1032
-
1033
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
1034
- const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
1035
- const nodeRuns = await planRunService.listNodeRuns(run.id)
1036
- const artifacts = await planRunService.listArtifacts(run.id)
1037
- const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
1038
- const emittedEvents: PlanEventRecord[] = []
1039
-
1040
- await databaseService.withTransaction(async (tx) => {
1041
- let currentNodeRuns = [...nodeRuns]
1042
- for (const nodeRun of currentNodeRuns.filter((candidate) => candidate.status === 'running')) {
1043
- const resetNodeRun = PlanNodeRunSchema.parse(
1044
- await tx
1045
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1046
- .content(
1047
- toNodeRunData(nodeRun, {
1048
- status: 'ready',
1049
- readyAt: new Date(),
1050
- startedAt: nodeRun.startedAt ?? new Date(),
1051
- }),
1052
- )
1053
- .output('after'),
1054
- )
1055
- currentNodeRuns = currentNodeRuns.map((candidate) =>
1056
- candidate.nodeId === resetNodeRun.nodeId ? resetNodeRun : candidate,
1057
- )
1058
- }
1059
-
1060
- const resetRun = await this.replaceRun(tx, run, {
1061
- status: run.status === 'awaiting-human' ? 'awaiting-human' : 'running',
1062
- currentNodeId: run.status === 'awaiting-human' ? (run.currentNodeId ?? null) : null,
1063
- waitingNodeId: run.status === 'awaiting-human' ? (run.waitingNodeId ?? null) : null,
1064
- readyNodeIds: currentNodeRuns
1065
- .filter((candidate) => candidate.status === 'ready')
1066
- .map((candidate) => candidate.nodeId),
1067
- })
1068
-
1069
- await this.emitEvent({
1070
- tx,
1071
- run: resetRun,
1072
- spec,
1073
- eventType: 'run-resumed',
1074
- fromStatus: run.status,
1075
- toStatus: resetRun.status,
1076
- message: `Run "${spec.title}" resumed from the latest checkpoint.`,
1077
- detail: latestCheckpoint ? { checkpointId: recordIdToString(latestCheckpoint.id, TABLES.PLAN_CHECKPOINT) } : {},
1078
- emittedBy: params.emittedBy,
1079
- capturedEvents: emittedEvents,
1080
- })
1081
-
1082
- const synced =
1083
- resetRun.status === 'awaiting-human'
1084
- ? { run: resetRun, nodeRuns: currentNodeRuns, artifacts }
1085
- : await this.syncRunGraph({
1086
- tx,
1087
- run: resetRun,
1088
- spec,
1089
- nodeSpecs,
1090
- nodeRuns: currentNodeRuns,
1091
- artifacts,
1092
- emittedBy: params.emittedBy,
1093
- capturedEvents: emittedEvents,
1094
- })
1095
-
1096
- const checkpoint = await this.saveCheckpoint({
1097
- tx,
1098
- run: synced.run,
1099
- spec,
1100
- nodeRuns: synced.nodeRuns,
1101
- artifacts: synced.artifacts,
1102
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
1103
- reason: 'run-resumed',
1104
- capturedEvents: emittedEvents,
1105
- })
1106
- await this.attachCheckpoint(tx, synced.run, checkpoint)
1107
- })
1108
-
1109
- await planEventDeliveryService.dispatchEvents(emittedEvents)
1110
-
1111
- const snapshot = await planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
1112
- includeEvents: true,
1113
- includeArtifacts: true,
1114
- includeApprovals: true,
1115
- includeCheckpoints: true,
1116
- includeValidationIssues: true,
1117
- })
1118
-
1119
- return buildExecutionPlanToolResult({
1120
- action: 'run-resumed',
1121
- plan: snapshot,
1122
- message: `Resumed execution run "${snapshot.title}".`,
1123
- })
1124
- }
1125
-
1126
- async transitionNodeToRunning(params: { runId: string; nodeId: string }): Promise<void> {
1127
- const run = await planRunService.getRunById(params.runId)
1128
- const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1129
- if (nodeRun.status !== 'ready') return
1130
-
1131
- await databaseService.withTransaction(async (tx) => {
1132
- const runningNodeRun = PlanNodeRunSchema.parse(
1133
- await tx
1134
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1135
- .content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? new Date() }))
1136
- .output('after'),
1137
- )
1138
-
1139
- const nodeRuns = await planRunService.listNodeRuns(run.id)
1140
- await this.replaceRun(tx, run, {
1141
- status: 'running',
1142
- currentNodeId: runningNodeRun.nodeId,
1143
- waitingNodeId: null,
1144
- readyNodeIds: nodeRuns
1145
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
1146
- .map((candidate) => candidate.nodeId),
1147
- })
1148
- })
1149
- }
1150
-
1151
- async blockNodeOnDispatchFailure(params: {
1152
- threadId: RecordIdInput
1153
- runId: string
1154
- nodeId: string
1155
- emittedBy: string
1156
- message: string
1157
- failureClass: PlanFailureClass
1158
- }): Promise<SerializableExecutionPlan> {
1159
- const run = await planRunService.getRunById(params.runId)
1160
-
1161
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
1162
- const nodeSpec = await planRunService.getNodeSpecByNodeId(spec.id, params.nodeId)
1163
- const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1164
- const artifacts = await planRunService.listArtifacts(run.id)
1165
- const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
1166
- const emittedEvents: PlanEventRecord[] = []
1167
-
1168
- await databaseService.withTransaction(async (tx) => {
1169
- const blockedNodeRun =
1170
- nodeRun.status === 'blocked'
1171
- ? nodeRun
1172
- : PlanNodeRunSchema.parse(
1173
- await tx
1174
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1175
- .content(
1176
- toNodeRunData(nodeRun, {
1177
- status: 'blocked',
1178
- blockedReason: params.message,
1179
- failureClass: params.failureClass,
1180
- }),
1181
- )
1182
- .output('after'),
1183
- )
1184
-
1185
- const blockedRun = await this.replaceRun(tx, run, {
1186
- status: 'blocked',
1187
- currentNodeId: blockedNodeRun.nodeId,
1188
- waitingNodeId: null,
1189
- readyNodeIds: [],
1190
- failureCount: run.failureCount + 1,
1191
- })
1192
-
1193
- await this.emitEvent({
1194
- tx,
1195
- run: blockedRun,
1196
- spec,
1197
- nodeId: blockedNodeRun.nodeId,
1198
- eventType: 'node-blocked',
1199
- fromStatus: nodeRun.status,
1200
- toStatus: blockedNodeRun.status,
1201
- message: `Node "${nodeSpec.label}" failed during owner dispatch.`,
1202
- detail: { failureClass: params.failureClass, phase: 'dispatch', error: params.message },
1203
- emittedBy: params.emittedBy,
1204
- capturedEvents: emittedEvents,
1205
- })
1206
-
1207
- const checkpoint = await this.saveCheckpoint({
1208
- tx,
1209
- run: blockedRun,
1210
- spec,
1211
- nodeRuns: (await planRunService.listNodeRuns(run.id)).map((candidate) =>
1212
- candidate.nodeId === blockedNodeRun.nodeId ? blockedNodeRun : candidate,
1213
- ),
1214
- artifacts,
1215
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
1216
- reason: 'owner-dispatch-failed',
1217
- capturedEvents: emittedEvents,
1218
- })
1219
- await this.attachCheckpoint(tx, blockedRun, checkpoint)
1220
- })
1221
-
1222
- await planEventDeliveryService.dispatchEvents(emittedEvents)
1223
-
1224
- return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
1225
- includeEvents: true,
1226
- includeArtifacts: true,
1227
- includeApprovals: true,
1228
- includeCheckpoints: true,
1229
- includeValidationIssues: true,
1230
- })
1231
- }
1232
-
1233
- async promoteDelayedNode(params: { runId: string; nodeId: string; emittedBy: string }): Promise<void> {
1234
- const run = await planRunService.getRunById(params.runId)
1235
- if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
1236
- return // Run is no longer active, skip promotion
1237
- }
1238
-
1239
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
1240
- const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
1241
- const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
1242
-
1243
- // Only promote if still in scheduled state (delay hasn't been superseded)
1244
- if (nodeRun.status !== 'scheduled') return
1245
-
1246
- const nodeRuns = await planRunService.listNodeRuns(run.id)
1247
- const artifacts = await planRunService.listArtifacts(run.id)
1248
- const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
1249
- const emittedEvents: PlanEventRecord[] = []
1250
-
1251
- await databaseService.withTransaction(async (tx) => {
1252
- const readyNodeRun = PlanNodeRunSchema.parse(
1253
- await tx
1254
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1255
- .content(toNodeRunData(nodeRun, { status: 'ready', readyAt: new Date() }))
1256
- .output('after'),
1257
- )
1258
-
1259
- const updatedNodeRuns = nodeRuns.map((candidate) =>
1260
- candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
1261
- )
1262
-
1263
- const nodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
1264
- await this.emitEvent({
1265
- tx,
1266
- run,
1267
- spec,
1268
- nodeId: readyNodeRun.nodeId,
1269
- eventType: 'node-ready',
1270
- fromStatus: nodeRun.status,
1271
- toStatus: readyNodeRun.status,
1272
- message: `Node "${nodeSpec?.label ?? params.nodeId}" promoted to ready after delay.`,
1273
- emittedBy: params.emittedBy,
1274
- capturedEvents: emittedEvents,
1275
- })
1276
-
1277
- const synced = await this.syncRunGraph({
1278
- tx,
1279
- run,
1280
- spec,
1281
- nodeSpecs,
1282
- nodeRuns: updatedNodeRuns,
1283
- artifacts,
1284
- emittedBy: params.emittedBy,
1285
- capturedEvents: emittedEvents,
1286
- })
1287
-
1288
- const checkpoint = await this.saveCheckpoint({
1289
- tx,
1290
- run: synced.run,
1291
- spec,
1292
- nodeRuns: synced.nodeRuns,
1293
- artifacts: synced.artifacts,
1294
- sequence: (latestCheckpoint?.sequence ?? 0) + 1,
1295
- reason: 'delayed-node-promoted',
1296
- capturedEvents: emittedEvents,
1297
- })
1298
- await this.attachCheckpoint(tx, synced.run, checkpoint)
1299
- })
1300
-
1301
- await planEventDeliveryService.dispatchEvents(emittedEvents)
1302
- }
1303
-
1304
- async syncRunGraph(params: {
1305
- tx: DatabaseTransaction
1306
- run: PlanRunRecord
1307
- spec: PlanSpecRecord
1308
- nodeSpecs: PlanNodeSpecRecord[]
1309
- nodeRuns: PlanNodeRunRecord[]
1310
- artifacts: Array<{
1311
- id: RecordIdInput
1312
- nodeId: string
1313
- name: string
1314
- kind: string
1315
- pointer: string
1316
- schemaRef?: string
1317
- payload?: unknown
1318
- }>
1319
- emittedBy: string
1320
- capturedEvents?: PlanEventRecord[]
1321
- }): Promise<{
1322
- run: PlanRunRecord
1323
- nodeRuns: PlanNodeRunRecord[]
1324
- artifacts: Array<{
1325
- id: RecordIdInput
1326
- nodeId: string
1327
- name: string
1328
- kind: string
1329
- pointer: string
1330
- schemaRef?: string
1331
- payload?: unknown
1332
- }>
1333
- }> {
1334
- let currentRun = params.run
1335
- let currentNodeRuns = [...params.nodeRuns]
1336
- const currentArtifacts = [...params.artifacts]
1337
- const sortedNodeSpecs = [...params.nodeSpecs].sort((left, right) => left.position - right.position)
1338
-
1339
- // Cross-plan dependency check: if spec has block-mode dependencies that are unresolved, block the run
1340
- if (params.spec.dependencies && params.spec.dependencies.length > 0) {
1341
- const { unresolved } = await planCoordinationService.resolveDependencies({
1342
- dependencies: params.spec.dependencies,
1343
- threadId: recordIdToString(params.spec.threadId, TABLES.THREAD),
1344
- })
1345
- if (unresolved.length > 0) {
1346
- currentRun = await this.replaceRun(params.tx, currentRun, { status: 'blocked', readyNodeIds: [] })
1347
- await this.emitEvent({
1348
- tx: params.tx,
1349
- run: currentRun,
1350
- spec: params.spec,
1351
- eventType: 'run-status-changed',
1352
- fromStatus: params.run.status,
1353
- toStatus: currentRun.status,
1354
- message: `Run blocked: unresolved cross-plan dependencies (${unresolved
1355
- .map((d) => d.sourcePlanSpecId)
1356
- .join(', ')}).`,
1357
- emittedBy: params.emittedBy,
1358
- capturedEvents: params.capturedEvents,
1359
- })
1360
- return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
1361
- }
1362
- }
1363
-
1364
- const replaceNodeRun = (nextNodeRun: PlanNodeRunRecord) => {
1365
- currentNodeRuns = currentNodeRuns.map((candidate) =>
1366
- candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
1367
- )
1368
- }
1369
-
1370
- const getNodeRunsById = () => new Map(currentNodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
1371
- const getArtifactsByNodeId = () =>
1372
- currentArtifacts.reduce((groups, artifact) => {
1373
- const list = groups.get(artifact.nodeId) ?? []
1374
- list.push(artifact)
1375
- groups.set(artifact.nodeId, list)
1376
- return groups
1377
- }, new Map<string, typeof currentArtifacts>())
1378
-
1379
- let changed = true
1380
- while (changed) {
1381
- changed = false
1382
- const nodeRunsById = getNodeRunsById()
1383
- const artifactsByNodeId = getArtifactsByNodeId()
1384
-
1385
- for (const nodeSpec of sortedNodeSpecs) {
1386
- const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
1387
- if (!nodeRun || nodeRun.status !== 'pending') continue
1388
-
1389
- const upstreamRuns = nodeSpec.upstreamNodeIds
1390
- .map((nodeId) => nodeRunsById.get(nodeId))
1391
- .filter(Boolean) as PlanNodeRunRecord[]
1392
- if (
1393
- nodeSpec.upstreamNodeIds.length > 0 &&
1394
- !upstreamRuns.every((upstreamRun) => isSuccessfulTerminalStatus(upstreamRun.status))
1395
- ) {
1396
- continue
1397
- }
1398
-
1399
- const activeIncomingEdges = params.spec.edges.filter((edge) => {
1400
- if (edge.target !== nodeSpec.nodeId) return false
1401
- const sourceRun = nodeRunsById.get(edge.source)
1402
- if (!sourceRun) return false
1403
- const context = buildNodeContext({ nodeRun: sourceRun, artifacts: artifactsByNodeId.get(edge.source) ?? [] })
1404
- return evaluateCondition(edge.when, context)
1405
- })
1406
-
1407
- if (nodeSpec.upstreamNodeIds.length > 0 && activeIncomingEdges.length === 0) {
1408
- const skippedNodeRun = PlanNodeRunSchema.parse(
1409
- await params.tx
1410
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1411
- .content(
1412
- toNodeRunData(nodeRun, {
1413
- status: 'skipped',
1414
- completedAt: new Date(),
1415
- blockedReason: null,
1416
- failureClass: null,
1417
- }),
1418
- )
1419
- .output('after'),
1420
- )
1421
- replaceNodeRun(skippedNodeRun)
1422
- await this.emitEvent({
1423
- tx: params.tx,
1424
- run: currentRun,
1425
- spec: params.spec,
1426
- nodeId: skippedNodeRun.nodeId,
1427
- eventType: 'node-skipped',
1428
- fromStatus: nodeRun.status,
1429
- toStatus: skippedNodeRun.status,
1430
- message: `Node "${nodeSpec.label}" was skipped because no inbound branch was activated.`,
1431
- emittedBy: params.emittedBy,
1432
- capturedEvents: params.capturedEvents,
1433
- })
1434
- changed = true
1435
- continue
1436
- }
1437
-
1438
- const resolvedInput = this.buildResolvedInput({ spec: params.spec, nodeSpec, nodeRunsById, artifactsByNodeId })
1439
-
1440
- const nodeSchedule = nodeSpec.schedule
1441
- const hasNonImmediateSchedule = nodeSchedule && nodeSchedule.type !== 'immediate'
1442
-
1443
- if (hasNonImmediateSchedule) {
1444
- const scheduledNodeRun = PlanNodeRunSchema.parse(
1445
- await params.tx
1446
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1447
- .content(toNodeRunData(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: new Date() }))
1448
- .output('after'),
1449
- )
1450
- replaceNodeRun(scheduledNodeRun)
1451
- await planSchedulerService.createSchedule({
1452
- organizationId: currentRun.organizationId,
1453
- threadId: currentRun.threadId,
1454
- planSpecId: params.spec.id,
1455
- runId: currentRun.id,
1456
- nodeId: nodeSpec.nodeId,
1457
- scheduleSpec: nodeSchedule,
1458
- })
1459
- await this.emitEvent({
1460
- tx: params.tx,
1461
- run: currentRun,
1462
- spec: params.spec,
1463
- nodeId: scheduledNodeRun.nodeId,
1464
- eventType: 'node-scheduled',
1465
- fromStatus: nodeRun.status,
1466
- toStatus: scheduledNodeRun.status,
1467
- message: `Node "${nodeSpec.label}" is scheduled (${nodeSchedule.type}).`,
1468
- emittedBy: params.emittedBy,
1469
- capturedEvents: params.capturedEvents,
1470
- })
1471
- changed = true
1472
- } else if (nodeSpec.delayAfterPredecessorMs) {
1473
- // Event-triggered delay: enqueue a delayed promotion instead of transitioning to ready immediately
1474
- const { enqueueDelayedNodePromotion } = await import('../queues/delayed-node-promotion.queue')
1475
- const scheduledNodeRun = PlanNodeRunSchema.parse(
1476
- await params.tx
1477
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1478
- .content(toNodeRunData(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: new Date() }))
1479
- .output('after'),
1480
- )
1481
- replaceNodeRun(scheduledNodeRun)
1482
- await enqueueDelayedNodePromotion(
1483
- {
1484
- runId: recordIdToString(currentRun.id, TABLES.PLAN_RUN),
1485
- nodeId: nodeSpec.nodeId,
1486
- emittedBy: params.emittedBy,
1487
- },
1488
- nodeSpec.delayAfterPredecessorMs,
1489
- )
1490
- await this.emitEvent({
1491
- tx: params.tx,
1492
- run: currentRun,
1493
- spec: params.spec,
1494
- nodeId: scheduledNodeRun.nodeId,
1495
- eventType: 'node-scheduled',
1496
- fromStatus: nodeRun.status,
1497
- toStatus: scheduledNodeRun.status,
1498
- message: `Node "${nodeSpec.label}" is delayed by ${nodeSpec.delayAfterPredecessorMs}ms after predecessor.`,
1499
- emittedBy: params.emittedBy,
1500
- capturedEvents: params.capturedEvents,
1501
- })
1502
- changed = true
1503
- } else {
1504
- const readyNodeRun = PlanNodeRunSchema.parse(
1505
- await params.tx
1506
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1507
- .content(toNodeRunData(nodeRun, { status: 'ready', resolvedInput, readyAt: new Date() }))
1508
- .output('after'),
1509
- )
1510
- replaceNodeRun(readyNodeRun)
1511
- await this.emitEvent({
1512
- tx: params.tx,
1513
- run: currentRun,
1514
- spec: params.spec,
1515
- nodeId: readyNodeRun.nodeId,
1516
- eventType: 'node-ready',
1517
- fromStatus: nodeRun.status,
1518
- toStatus: readyNodeRun.status,
1519
- message: `Node "${nodeSpec.label}" is ready to execute.`,
1520
- emittedBy: params.emittedBy,
1521
- capturedEvents: params.capturedEvents,
1522
- })
1523
- changed = true
1524
- }
1525
- }
1526
-
1527
- const readyStructuralNodes = sortedNodeSpecs.filter((nodeSpec) => {
1528
- const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
1529
- return nodeRun?.status === 'ready' && isStructuralNodeType(nodeSpec.type)
1530
- })
1531
-
1532
- for (const nodeSpec of readyStructuralNodes) {
1533
- const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
1534
- if (!nodeRun) continue
1535
-
1536
- const completedNodeRun = PlanNodeRunSchema.parse(
1537
- await params.tx
1538
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1539
- .content(
1540
- toNodeRunData(nodeRun, {
1541
- status: 'completed',
1542
- startedAt: nodeRun.startedAt ?? new Date(),
1543
- completedAt: new Date(),
1544
- }),
1545
- )
1546
- .output('after'),
1547
- )
1548
- replaceNodeRun(completedNodeRun)
1549
- await this.emitEvent({
1550
- tx: params.tx,
1551
- run: currentRun,
1552
- spec: params.spec,
1553
- nodeId: completedNodeRun.nodeId,
1554
- eventType: 'node-auto-completed',
1555
- fromStatus: nodeRun.status,
1556
- toStatus: completedNodeRun.status,
1557
- message: `Structural node "${nodeSpec.label}" auto-completed.`,
1558
- emittedBy: params.emittedBy,
1559
- capturedEvents: params.capturedEvents,
1560
- })
1561
- changed = true
1562
- }
1563
- }
1564
-
1565
- const nodeRunsById = getNodeRunsById()
1566
- const activeRunningNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'running')
1567
- const activeHumanNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'awaiting-human')
1568
- const activeMonitoringNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'monitoring')
1569
- const hasScheduledOrMonitoring = currentNodeRuns.some(
1570
- (nodeRun) => nodeRun.status === 'scheduled' || nodeRun.status === 'monitoring',
1571
- )
1572
-
1573
- if (!activeRunningNode && !activeHumanNode && !activeMonitoringNode) {
1574
- const nextHumanNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
1575
- const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
1576
- return nodeRun?.status === 'ready' && isHumanNodeType(nodeSpec.type)
1577
- })
1578
-
1579
- if (nextHumanNodeSpec) {
1580
- const nodeRun = nodeRunsById.get(nextHumanNodeSpec.nodeId)
1581
- if (!nodeRun) {
1582
- throw new Error(`Expected ready node run for "${nextHumanNodeSpec.nodeId}".`)
1583
- }
1584
- const awaitingHumanNodeRun = PlanNodeRunSchema.parse(
1585
- await params.tx
1586
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1587
- .content(toNodeRunData(nodeRun, { status: 'awaiting-human', startedAt: nodeRun.startedAt ?? new Date() }))
1588
- .output('after'),
1589
- )
1590
- replaceNodeRun(awaitingHumanNodeRun)
1591
-
1592
- const approval = await planApprovalService.createPendingApproval({
1593
- tx: params.tx,
1594
- runId: currentRun.id,
1595
- nodeRunId: awaitingHumanNodeRun.id,
1596
- nodeId: awaitingHumanNodeRun.nodeId,
1597
- requestedBy: params.emittedBy,
1598
- presented: {
1599
- nodeId: nextHumanNodeSpec.nodeId,
1600
- label: nextHumanNodeSpec.label,
1601
- objective: nextHumanNodeSpec.objective,
1602
- instructions: nextHumanNodeSpec.instructions,
1603
- deliverables: nextHumanNodeSpec.deliverables,
1604
- successCriteria: nextHumanNodeSpec.successCriteria,
1605
- resolvedInput: awaitingHumanNodeRun.resolvedInput ?? {},
1606
- },
1607
- })
1608
-
1609
- currentRun = await this.replaceRun(params.tx, currentRun, {
1610
- status: 'awaiting-human',
1611
- currentNodeId: awaitingHumanNodeRun.nodeId,
1612
- waitingNodeId: awaitingHumanNodeRun.nodeId,
1613
- readyNodeIds: currentNodeRuns
1614
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== awaitingHumanNodeRun.nodeId)
1615
- .map((candidate) => candidate.nodeId),
1616
- })
1617
-
1618
- await this.emitEvent({
1619
- tx: params.tx,
1620
- run: currentRun,
1621
- spec: params.spec,
1622
- nodeId: awaitingHumanNodeRun.nodeId,
1623
- approvalId: approval.id,
1624
- eventType: 'approval-requested',
1625
- fromStatus: params.run.status,
1626
- toStatus: currentRun.status,
1627
- message: `Node "${nextHumanNodeSpec.label}" is awaiting human input.`,
1628
- emittedBy: params.emittedBy,
1629
- capturedEvents: params.capturedEvents,
1630
- })
1631
- } else {
1632
- const nextActionNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
1633
- const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
1634
- return nodeRun?.status === 'ready' && !isStructuralNodeType(nodeSpec.type)
1635
- })
1636
-
1637
- if (nextActionNodeSpec) {
1638
- const nodeRun = nodeRunsById.get(nextActionNodeSpec.nodeId)
1639
- if (!nodeRun) {
1640
- throw new Error(`Expected ready node run for "${nextActionNodeSpec.nodeId}".`)
1641
- }
1642
- const runningNodeRun = PlanNodeRunSchema.parse(
1643
- await params.tx
1644
- .update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
1645
- .content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? new Date() }))
1646
- .output('after'),
1647
- )
1648
- replaceNodeRun(runningNodeRun)
1649
-
1650
- currentRun = await this.replaceRun(params.tx, currentRun, {
1651
- status: 'running',
1652
- currentNodeId: runningNodeRun.nodeId,
1653
- waitingNodeId: null,
1654
- readyNodeIds: currentNodeRuns
1655
- .filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
1656
- .map((candidate) => candidate.nodeId),
1657
- })
1658
-
1659
- await this.emitEvent({
1660
- tx: params.tx,
1661
- run: currentRun,
1662
- spec: params.spec,
1663
- nodeId: runningNodeRun.nodeId,
1664
- eventType: 'node-running',
1665
- fromStatus: nodeRun.status,
1666
- toStatus: runningNodeRun.status,
1667
- message: `Node "${nextActionNodeSpec.label}" is now running.`,
1668
- emittedBy: params.emittedBy,
1669
- capturedEvents: params.capturedEvents,
1670
- })
1671
- await this.emitEvent({
1672
- tx: params.tx,
1673
- run: currentRun,
1674
- spec: params.spec,
1675
- nodeId: runningNodeRun.nodeId,
1676
- eventType: 'ownership-transition',
1677
- fromStatus: params.run.currentNodeId ?? undefined,
1678
- toStatus: runningNodeRun.nodeId,
1679
- message: `Execution ownership transitioned to "${nextActionNodeSpec.label}".`,
1680
- detail: { owner: nextActionNodeSpec.owner },
1681
- emittedBy: params.emittedBy,
1682
- capturedEvents: params.capturedEvents,
1683
- })
1684
- } else if (currentNodeRuns.every((nodeRun) => isSuccessfulTerminalStatus(nodeRun.status))) {
1685
- currentRun = await this.replaceRun(params.tx, currentRun, {
1686
- status: 'completed',
1687
- currentNodeId: null,
1688
- waitingNodeId: null,
1689
- readyNodeIds: [],
1690
- completedAt: new Date(),
1691
- })
1692
-
1693
- await this.emitEvent({
1694
- tx: params.tx,
1695
- run: currentRun,
1696
- spec: params.spec,
1697
- eventType: 'run-status-changed',
1698
- fromStatus: params.run.status,
1699
- toStatus: currentRun.status,
1700
- message: `Run "${params.spec.title}" completed.`,
1701
- emittedBy: params.emittedBy,
1702
- capturedEvents: params.capturedEvents,
1703
- })
1704
- } else if (hasScheduledOrMonitoring) {
1705
- // Nodes are waiting on schedules/monitors — run stays active
1706
- currentRun = await this.replaceRun(params.tx, currentRun, {
1707
- status: 'running',
1708
- currentNodeId: null,
1709
- waitingNodeId: null,
1710
- readyNodeIds: currentNodeRuns
1711
- .filter((candidate) => candidate.status === 'ready')
1712
- .map((candidate) => candidate.nodeId),
1713
- })
1714
- } else {
1715
- currentRun = await this.replaceRun(params.tx, currentRun, {
1716
- status: 'blocked',
1717
- currentNodeId: null,
1718
- waitingNodeId: null,
1719
- readyNodeIds: currentNodeRuns
1720
- .filter((candidate) => candidate.status === 'ready')
1721
- .map((candidate) => candidate.nodeId),
1722
- })
1723
- }
1724
- }
1725
- } else {
1726
- currentRun = await this.replaceRun(params.tx, currentRun, {
1727
- status: activeHumanNode ? 'awaiting-human' : 'running',
1728
- currentNodeId: activeHumanNode?.nodeId ?? activeMonitoringNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
1729
- waitingNodeId: activeHumanNode?.nodeId ?? null,
1730
- readyNodeIds: currentNodeRuns
1731
- .filter((candidate) => candidate.status === 'ready')
1732
- .map((candidate) => candidate.nodeId),
1733
- })
1734
- }
1735
-
1736
- return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
1737
- }
1738
-
1739
- private resolveFailureAction(nodeSpec: PlanNodeSpecRecord, failureClass: PlanFailureClass | null): PlanFailureAction {
1740
- if (!failureClass) return 'abort'
1741
-
1742
- const matchedRule = nodeSpec.failurePolicy.find((rule) => rule.on === failureClass)
1743
- return matchedRule?.action ?? 'abort'
1744
- }
1745
-
1746
- private buildResolvedInput(params: {
1747
- spec: PlanSpecRecord
1748
- nodeSpec: PlanNodeSpecRecord
1749
- nodeRunsById: Map<string, PlanNodeRunRecord>
1750
- artifactsByNodeId: Map<
1751
- string,
1752
- Array<{ nodeId: string; name: string; kind: string; pointer: string; schemaRef?: string; payload?: unknown }>
1753
- >
1754
- }) {
1755
- const resolvedInput: Record<string, unknown> = {}
1756
-
1757
- for (const edge of params.spec.edges.filter((candidate) => candidate.target === params.nodeSpec.nodeId)) {
1758
- const sourceRun = params.nodeRunsById.get(edge.source)
1759
- if (!sourceRun || !isSuccessfulTerminalStatus(sourceRun.status)) continue
1760
-
1761
- const context = buildNodeContext({
1762
- nodeRun: sourceRun,
1763
- artifacts: params.artifactsByNodeId.get(edge.source) ?? [],
1764
- })
1765
- if (!evaluateCondition(edge.when, context)) continue
1766
-
1767
- for (const [targetPath, sourcePath] of Object.entries(edge.map)) {
1768
- const value = readPathValue(context, sourcePath)
1769
- if (value !== undefined) {
1770
- setPathValue(resolvedInput, targetPath, value)
1771
- }
1772
- }
1773
- }
1774
-
1775
- return resolvedInput
1776
- }
1777
-
1778
- private async createAttempt(params: {
1779
- tx: DatabaseTransaction
1780
- run: PlanRunRecord
1781
- nodeRun: PlanNodeRunRecord
1782
- emittedBy: string
1783
- result: PlanNodeResultSubmission
1784
- status: 'completed' | 'failed'
1785
- failureClass: PlanFailureClass | null
1786
- }) {
1787
- const attemptId = new RecordId(TABLES.PLAN_NODE_ATTEMPT, Bun.randomUUIDv7())
1788
- return PlanNodeAttemptSchema.parse(
1789
- await params.tx
1790
- .create(attemptId)
1791
- .content({
1792
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
1793
- nodeRunId: ensureRecordId(params.nodeRun.id, TABLES.PLAN_NODE_RUN),
1794
- nodeId: params.nodeRun.nodeId,
1795
- emittedBy: params.emittedBy,
1796
- status: params.status,
1797
- ...(params.result.structuredOutput ? { structuredOutput: params.result.structuredOutput } : {}),
1798
- ...(params.result.notes ? { notes: params.result.notes } : {}),
1799
- validationIssueIds: [],
1800
- ...(params.failureClass ? { failureClass: params.failureClass } : {}),
1801
- })
1802
- .output('after'),
1803
- )
1804
- }
1805
-
1806
- private async persistValidationIssues(params: {
1807
- tx: DatabaseTransaction
1808
- run: PlanRunRecord
1809
- spec: PlanSpecRecord
1810
- attemptId: RecordIdInput
1811
- nodeId: string
1812
- issues: PlanValidationIssueInput[]
1813
- }): Promise<PlanValidationIssueRecord[]> {
1814
- const records: PlanValidationIssueRecord[] = []
1815
-
1816
- for (const issue of params.issues) {
1817
- const issueId = new RecordId(TABLES.PLAN_VALIDATION_ISSUE, Bun.randomUUIDv7())
1818
- const created = await params.tx
1819
- .create(issueId)
1820
- .content({
1821
- planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
1822
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
1823
- nodeId: issue.nodeId ?? params.nodeId,
1824
- attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT),
1825
- severity: issue.severity,
1826
- code: issue.code,
1827
- message: issue.message,
1828
- ...(issue.detail ? { detail: issue.detail } : {}),
1829
- })
1830
- .output('after')
1831
-
1832
- records.push(PlanValidationIssueSchema.parse(created))
1833
- }
1834
-
1835
- return records
1836
- }
1837
-
1838
- private async emitEvent(params: {
1839
- tx: DatabaseTransaction
1840
- run: PlanRunRecord
1841
- spec: PlanSpecRecord
1842
- eventType: PlanEventType
1843
- message: string
1844
- emittedBy: string
1845
- nodeId?: string
1846
- attemptId?: RecordIdInput
1847
- approvalId?: RecordIdInput
1848
- fromStatus?: string
1849
- toStatus?: string
1850
- detail?: Record<string, unknown>
1851
- capturedEvents?: PlanEventRecord[]
1852
- }): Promise<PlanEventRecord> {
1853
- const eventId = new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7())
1854
- const created = await params.tx
1855
- .create(eventId)
1856
- .content({
1857
- planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
1858
- runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
1859
- eventType: params.eventType,
1860
- message: params.message,
1861
- emittedBy: params.emittedBy,
1862
- ...(params.nodeId ? { nodeId: params.nodeId } : {}),
1863
- ...(params.attemptId ? { attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT) } : {}),
1864
- ...(params.approvalId ? { approvalId: ensureRecordId(params.approvalId, TABLES.PLAN_APPROVAL) } : {}),
1865
- ...(params.fromStatus ? { fromStatus: params.fromStatus } : {}),
1866
- ...(params.toStatus ? { toStatus: params.toStatus } : {}),
1867
- ...(params.detail ? { detail: params.detail } : {}),
1868
- })
1869
- .output('after')
1870
-
1871
- const event = PlanEventSchema.parse(created)
1872
- params.capturedEvents?.push(event)
1873
- return event
1874
- }
1875
-
1876
- private async replaceRun(tx: DatabaseTransaction, run: PlanRunRecord, patch: PlanRunUpdate): Promise<PlanRunRecord> {
1877
- return PlanRunSchema.parse(
1878
- await tx.update(ensureRecordId(run.id, TABLES.PLAN_RUN)).content(toRunData(run, patch)).output('after'),
1879
- )
1880
- }
1881
-
1882
- private async saveCheckpoint(params: {
1883
- tx: DatabaseTransaction
1884
- run: PlanRunRecord
1885
- spec: PlanSpecRecord
1886
- nodeRuns: PlanNodeRunRecord[]
1887
- artifacts: Array<{ id: RecordIdInput; nodeId: string }>
1888
- sequence: number
1889
- reason: string
1890
- includeWorkspace?: boolean
1891
- capturedEvents?: PlanEventRecord[]
1892
- }) {
1893
- const checkpoint = await planCheckpointService.createCheckpoint({
1894
- tx: params.tx,
1895
- runId: params.run.id,
1896
- sequence: params.sequence,
1897
- runStatus: params.run.status,
1898
- readyNodeIds: params.run.readyNodeIds,
1899
- activeNodeIds: params.run.currentNodeId ? [params.run.currentNodeId] : [],
1900
- artifactIds: params.artifacts.map((artifact) => artifact.id),
1901
- lastCompletedNodeIds: params.nodeRuns
1902
- .filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
1903
- .map((nodeRun) => nodeRun.nodeId),
1904
- snapshot: {
1905
- reason: params.reason,
1906
- runStatus: params.run.status,
1907
- currentNodeId: params.run.currentNodeId,
1908
- waitingNodeId: params.run.waitingNodeId,
1909
- readyNodeIds: params.run.readyNodeIds,
1910
- nodeStatuses: Object.fromEntries(params.nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun.status])),
1911
- },
1912
- includeWorkspace: params.includeWorkspace,
1913
- })
1914
-
1915
- await this.emitEvent({
1916
- tx: params.tx,
1917
- run: params.run,
1918
- spec: params.spec,
1919
- eventType: 'checkpoint-saved',
1920
- message: `Saved checkpoint ${checkpoint.sequence}.`,
1921
- detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: params.reason },
1922
- emittedBy: 'system',
1923
- capturedEvents: params.capturedEvents,
1924
- })
1925
-
1926
- return checkpoint
1927
- }
1928
-
1929
- private async attachCheckpoint(
1930
- tx: DatabaseTransaction,
1931
- run: PlanRunRecord,
1932
- checkpoint: RecordIdInput | { id: RecordIdInput },
1933
- ) {
1934
- const checkpointId =
1935
- checkpoint &&
1936
- typeof checkpoint === 'object' &&
1937
- !(checkpoint instanceof RecordId) &&
1938
- !(checkpoint instanceof StringRecordId) &&
1939
- 'id' in checkpoint
1940
- ? (checkpoint as { id: RecordIdInput }).id
1941
- : checkpoint
1942
-
1943
- await tx
1944
- .update(ensureRecordId(run.id, TABLES.PLAN_RUN))
1945
- .content(toRunData(run, { lastCheckpointId: checkpointId }))
1946
- .output('after')
1947
- }
1948
- }
1949
-
1950
- export const planExecutorService = new PlanExecutorService()