@lota-sdk/core 0.4.8 → 0.4.10

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