@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
@@ -11,28 +11,62 @@ import type {
11
11
  PlanRunRecord,
12
12
  PlanSchemaRegistry,
13
13
  PlanSpecRecord,
14
+ PlanNodeOwner,
14
15
  PlanDraft,
15
- SerializableExecutionPlan,
16
16
  UpstreamHandoff,
17
17
  } from '@lota-sdk/shared'
18
+ import { Cause, Context, Effect, Exit, Layer, Match } from 'effect'
18
19
 
19
- import { agentRoster } from '../config/agent-defaults'
20
+ import type { ResolvedAgentConfig } from '../config/agent-defaults'
20
21
  import type { RecordIdInput } from '../db/record-id'
21
22
  import { ensureRecordId, recordIdToString } from '../db/record-id'
22
- import { databaseService } from '../db/service'
23
+ import type { SurrealDBService } from '../db/service'
23
24
  import { TABLES } from '../db/tables'
25
+ import { BadRequestError, ConfigurationError, DatabaseError, ServiceError } from '../effect/errors'
26
+ import { isPromiseLike, makeEffectTryPromiseWithMessage } from '../effect/helpers'
27
+ import { AgentConfigServiceTag, DatabaseServiceTag, RuntimeAdaptersServiceTag } from '../effect/services'
24
28
  import { resolvePlanNodeExecutionVisibility, shouldPlanNodeUseVisibleTurn } from '../runtime/execution-plan-visibility'
25
- import { getRuntimeAdapters } from '../runtime/runtime-extensions'
26
- import { agentExecutorService } from './agent-executor.service'
27
- import { monitoringWindowService } from './monitoring-window.service'
28
- import { planExecutorService } from './plan-executor.service'
29
- import { planRunService } from './plan-run.service'
30
- import type { PlanValidationIssueInput } from './plan-validator.service'
31
- import { pluginExecutorService } from './plugin-executor.service'
32
- import { skillResolverService } from './skill-resolver.service'
33
- import { systemExecutorService } from './system-executor.service'
34
- import { ThreadSchema } from './thread.types'
35
- import { userService } from './user.service'
29
+ import type { LotaRuntimeAdapters } from '../runtime/runtime-extensions'
30
+ import type { makeAgentExecutorService } from './agent-executor.service'
31
+ import { AgentExecutorServiceTag } from './agent-executor.service'
32
+ import { routeGraphFullEffect } from './graph-full-routing'
33
+ import type { makeMonitoringWindowService } from './monitoring-window.service'
34
+ import { MonitoringWindowServiceTag } from './monitoring-window.service'
35
+ import type { makePlanExecutorService } from './plan/plan-executor.service'
36
+ import { PlanExecutorServiceTag } from './plan/plan-executor.service'
37
+ import { serializeRunFull } from './plan/plan-run-serialization'
38
+ import type { makePlanRunService } from './plan/plan-run.service'
39
+ import { PlanRunServiceTag } from './plan/plan-run.service'
40
+ import type { PlanValidationIssueInput } from './plan/plan-validator.service'
41
+ import type { makePluginExecutorService } from './plugin-executor.service'
42
+ import { PluginExecutorServiceTag } from './plugin-executor.service'
43
+ import type { SkillResolverService } from './skill-resolver.service'
44
+ import { SkillResolverServiceTag } from './skill-resolver.service'
45
+ import type { makeSystemExecutorService } from './system-executor.service'
46
+ import { SystemExecutorServiceTag } from './system-executor.service'
47
+ import { ThreadSchema } from './thread/thread.types'
48
+ import type { makeUserService } from './user.service'
49
+ import { UserServiceTag } from './user.service'
50
+
51
+ interface OwnershipDispatcherDeps {
52
+ db: SurrealDBService
53
+ agentConfig: ResolvedAgentConfig
54
+ runtimeAdapters: LotaRuntimeAdapters
55
+ agentExecutor: NoContextService<Pick<ReturnType<typeof makeAgentExecutorService>, 'executeNode'>>
56
+ monitoringWindow: NoContextService<Pick<ReturnType<typeof makeMonitoringWindowService>, 'startMonitoringWindow'>>
57
+ planExecutor: ReturnType<typeof makePlanExecutorService>
58
+ planRun: ReturnType<typeof makePlanRunService>
59
+ pluginExecutor: NoContextService<Pick<ReturnType<typeof makePluginExecutorService>, 'executeNode' | 'validateOwner'>>
60
+ skillResolver: NoContextService<Pick<SkillResolverService, 'resolve'>>
61
+ systemExecutor: NoContextService<Pick<ReturnType<typeof makeSystemExecutorService>, 'executeNode' | 'validateOwner'>>
62
+ user: NoContextService<Pick<ReturnType<typeof makeUserService>, 'getUser'>>
63
+ }
64
+
65
+ type NoContextService<TService> = {
66
+ [K in keyof TService]: TService[K] extends (...args: infer TArgs) => Effect.Effect<infer A, infer E, unknown>
67
+ ? (...args: TArgs) => Effect.Effect<A, E, never>
68
+ : TService[K]
69
+ }
36
70
 
37
71
  const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
38
72
  const MAX_DISPATCH_ITERATIONS = 64
@@ -101,203 +135,93 @@ function formatDispatchError(error: unknown): string {
101
135
  return error instanceof Error ? error.message : String(error)
102
136
  }
103
137
 
104
- class OwnershipDispatcherService {
105
- validateDraftExecutors(draft: PlanDraft): PlanValidationIssueInput[] {
106
- const issues: PlanValidationIssueInput[] = []
107
-
108
- for (const node of draft.nodes) {
109
- if (node.owner.executorType === 'agent') {
110
- if (!agentRoster.includes(node.owner.ref)) {
111
- issues.push({
112
- severity: 'blocking',
113
- code: 'agent_executor_missing',
114
- message: `Node "${node.label}" references unknown agent executor "${node.owner.ref}".`,
115
- nodeId: node.id,
116
- detail: { agentId: node.owner.ref },
117
- })
118
- }
119
- continue
120
- }
121
-
122
- if (node.owner.executorType === 'plugin') {
123
- issues.push(...pluginExecutorService.validateOwner(node.owner, node.id))
124
- continue
125
- }
126
-
127
- if (node.owner.executorType === 'system') {
128
- issues.push(...systemExecutorService.validateOwner(node.owner, node.id))
129
- continue
130
- }
131
-
132
- if (node.owner.executorType === 'skill') {
133
- // Skill owners are validated at execution time via skillResolverService.
134
- }
135
- }
136
-
137
- return issues
138
- }
139
-
140
- async dispatchRunToStableBoundary(params: {
141
- runId: RecordIdInput
142
- emittedBy: string
143
- }): Promise<SerializableExecutionPlan> {
144
- const initialRun = await planRunService.getRunById(params.runId)
145
- const autoDispatchEnabled = await this.shouldAutoDispatch(initialRun)
146
- if (!autoDispatchEnabled) {
147
- return this.serializeRun(initialRun.id)
148
- }
149
-
150
- let iteration = 0
151
- while (iteration < MAX_DISPATCH_ITERATIONS) {
152
- const run = await planRunService.getRunById(params.runId)
153
- if (STABLE_RUN_STATUSES.has(run.status) || run.status !== 'running' || !run.currentNodeId) {
154
- return this.serializeRun(run.id)
155
- }
156
-
157
- const spec = await planRunService.getPlanSpecById(run.planSpecId)
158
-
159
- if (spec.executionMode === 'graph-full') {
160
- const { globalOrchestratorService } = await import('./global-orchestrator.service')
161
- await globalOrchestratorService.routeGraphFull({
162
- threadId: recordIdToString(run.threadId, TABLES.THREAD),
163
- runId: recordIdToString(run.id, TABLES.PLAN_RUN),
164
- })
165
- return this.serializeRun(run.id)
166
- }
167
-
168
- const nodeSpecRecord = await planRunService.getNodeSpecByNodeId(spec.id, run.currentNodeId)
169
- const planNode = toPlanNodeSpec(nodeSpecRecord)
170
- if (planNode.owner.executorType === 'user') {
171
- return this.serializeRun(run.id)
172
- }
173
-
174
- const nodeRun = await planRunService.getNodeRunByNodeId(run.id, nodeSpecRecord.nodeId)
175
- if (nodeRun.status === 'monitoring') {
176
- // Monitoring nodes are managed by the scheduler — treat as stable
177
- return this.serializeRun(run.id)
178
- }
179
- if (nodeRun.status !== 'running') {
180
- return this.serializeRun(run.id)
181
- }
182
- if (shouldPlanNodeUseVisibleTurn(spec, nodeSpecRecord)) {
183
- return this.serializeRun(run.id)
184
- }
185
-
186
- const [artifacts, dispatchContext] = await Promise.all([
187
- planRunService.listArtifacts(run.id),
188
- this.buildDispatchContext(run, spec, nodeSpecRecord),
189
- ])
190
- const inputArtifacts = artifacts
191
- .filter((artifact) => nodeSpecRecord.upstreamNodeIds.includes(artifact.nodeId))
192
- .map((artifact) => toArtifactSubmission(artifact))
193
-
194
- try {
195
- const result = await this.dispatchNode({
196
- nodeSpec: planNode,
197
- resolvedInput: nodeRun.resolvedInput ?? {},
198
- inputArtifacts,
199
- context: { ...dispatchContext, nodeId: planNode.id },
200
- executionMode: spec.executionMode,
201
- schemaRegistry: spec.schemaRegistry,
202
- })
138
+ function toDispatchDatabaseError(message: string, cause: unknown) {
139
+ return new DatabaseError({ message, cause })
140
+ }
203
141
 
204
- await planExecutorService.submitNodeResult({
205
- threadId: run.threadId,
206
- runId: recordIdToString(run.id, TABLES.PLAN_RUN),
207
- nodeId: planNode.id,
208
- emittedBy: planNode.owner.ref,
209
- result,
210
- })
211
- } catch (error) {
212
- await planExecutorService.blockNodeOnDispatchFailure({
213
- threadId: run.threadId,
214
- runId: recordIdToString(run.id, TABLES.PLAN_RUN),
215
- nodeId: planNode.id,
216
- emittedBy: planNode.owner.ref,
217
- message: formatDispatchError(error),
218
- failureClass: classifyDispatchFailure({ ownerType: planNode.owner.executorType, error }),
219
- })
220
- return await this.serializeRun(run.id)
221
- }
142
+ const tryDispatchPromise = makeEffectTryPromiseWithMessage((message, cause) => new ServiceError({ message, cause }))
143
+
144
+ const matchDraftExecutor = (deps: OwnershipDispatcherDeps, node: { id: string; label: string }) =>
145
+ Match.type<PlanNodeOwner>().pipe(
146
+ Match.discriminator('executorType')('agent', (owner): PlanValidationIssueInput[] =>
147
+ deps.agentConfig.roster.includes(owner.ref)
148
+ ? []
149
+ : [
150
+ {
151
+ severity: 'blocking',
152
+ code: 'agent_executor_missing',
153
+ message: `Node "${node.label}" references unknown agent executor "${owner.ref}".`,
154
+ nodeId: node.id,
155
+ detail: { agentId: owner.ref },
156
+ },
157
+ ],
158
+ ),
159
+ Match.discriminator('executorType')('plugin', (owner): PlanValidationIssueInput[] =>
160
+ deps.pluginExecutor.validateOwner(owner, node.id),
161
+ ),
162
+ Match.discriminator('executorType')('system', (owner): PlanValidationIssueInput[] =>
163
+ deps.systemExecutor.validateOwner(owner, node.id),
164
+ ),
165
+ Match.discriminator('executorType')('skill', (): PlanValidationIssueInput[] => []),
166
+ Match.discriminator('executorType')('user', (): PlanValidationIssueInput[] => []),
167
+ Match.exhaustive,
168
+ )
169
+
170
+ function validateDraftExecutors(deps: OwnershipDispatcherDeps, draft: PlanDraft): PlanValidationIssueInput[] {
171
+ return draft.nodes.flatMap((node) => matchDraftExecutor(deps, node)(node.owner))
172
+ }
222
173
 
223
- iteration += 1
174
+ const shouldAutoDispatchEffect = (deps: OwnershipDispatcherDeps, run: PlanRunRecord) =>
175
+ Effect.gen(function* () {
176
+ const workspaceProvider = deps.runtimeAdapters.workspaceProvider
177
+ if (!workspaceProvider) {
178
+ return true
224
179
  }
225
180
 
226
- throw new Error(
227
- `Ownership dispatch exceeded ${MAX_DISPATCH_ITERATIONS} iterations for run ${recordIdToString(
228
- ensureRecordId(params.runId, TABLES.PLAN_RUN),
229
- TABLES.PLAN_RUN,
230
- )}.`,
181
+ const workspace = yield* tryDispatchPromise(
182
+ () => workspaceProvider.getWorkspace(ensureRecordId(run.organizationId, TABLES.ORGANIZATION)),
183
+ 'Failed to load workspace for dispatch eligibility.',
231
184
  )
232
- }
233
-
234
- async dispatchReadyNode(params: {
235
- run: PlanRunRecord
236
- nodeSpecRecord: PlanNodeSpecRecord
237
- nodeRun: PlanNodeRunRecord
238
- spec: PlanSpecRecord
239
- executionModeOverride?: ExecutionMode
240
- }): Promise<PlanNodeResultSubmission> {
241
- if (shouldPlanNodeUseVisibleTurn(params.spec, params.nodeSpecRecord)) {
242
- throw new Error(
243
- `Node "${params.nodeSpecRecord.nodeId}" requires a visible plan turn and cannot be silently dispatched.`,
244
- )
245
- }
246
-
247
- const planNode = toPlanNodeSpec(params.nodeSpecRecord)
248
- const [artifacts, dispatchContext] = await Promise.all([
249
- planRunService.listArtifacts(params.run.id),
250
- this.buildDispatchContext(params.run, params.spec, params.nodeSpecRecord),
251
- ])
252
- const inputArtifacts = artifacts
253
- .filter((artifact) => params.nodeSpecRecord.upstreamNodeIds.includes(artifact.nodeId))
254
- .map((artifact) => toArtifactSubmission(artifact))
255
-
256
- return this.dispatchNode({
257
- nodeSpec: planNode,
258
- resolvedInput: params.nodeRun.resolvedInput ?? {},
259
- inputArtifacts,
260
- context: { ...dispatchContext, nodeId: planNode.id },
261
- executionMode: params.spec.executionMode,
262
- executionModeOverride: params.executionModeOverride,
263
- schemaRegistry: params.spec.schemaRegistry,
264
- })
265
- }
266
-
267
- private async shouldAutoDispatch(run: PlanRunRecord): Promise<boolean> {
268
- const workspaceProvider = getRuntimeAdapters().workspaceProvider
269
- if (!workspaceProvider) {
185
+ if (!workspaceProvider.getLifecycleState) {
270
186
  return true
271
187
  }
272
188
 
273
- const workspace = await workspaceProvider.getWorkspace(ensureRecordId(run.organizationId, TABLES.ORGANIZATION))
274
- const lifecycleState = await workspaceProvider.getLifecycleState?.(workspace)
275
- return lifecycleState?.bootstrapActive !== true
276
- }
277
-
278
- resolveExecutionVisibility(params: { spec: PlanSpecRecord; nodeSpecRecord: PlanNodeSpecRecord }) {
279
- return resolvePlanNodeExecutionVisibility(params.spec, params.nodeSpecRecord)
280
- }
189
+ const lifecycleState = yield* Effect.gen(function* () {
190
+ const result = workspaceProvider.getLifecycleState?.call(workspaceProvider, workspace)
191
+ if (isPromiseLike(result)) {
192
+ return yield* tryDispatchPromise(() => result, 'Failed to read workspace lifecycle state.')
193
+ }
281
194
 
282
- private async buildDispatchContext(
283
- run: PlanRunRecord,
284
- spec: PlanSpecRecord,
285
- nodeSpecRecord: PlanNodeSpecRecord,
286
- ): Promise<Omit<OwnershipDispatchContext, 'nodeId'>> {
195
+ return result
196
+ })
197
+ return lifecycleState ? lifecycleState.bootstrapActive !== true : true
198
+ })
199
+
200
+ const buildDispatchContextEffect = (
201
+ deps: OwnershipDispatcherDeps,
202
+ run: PlanRunRecord,
203
+ spec: PlanSpecRecord,
204
+ nodeSpecRecord: PlanNodeSpecRecord,
205
+ ) =>
206
+ Effect.gen(function* () {
287
207
  const organizationId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
288
208
  const threadId = recordIdToString(run.threadId, TABLES.THREAD)
289
209
  const planId = recordIdToString(run.id, TABLES.PLAN_RUN)
290
- const [thread, nodeSpecs, nodeRuns] = await Promise.all([
291
- databaseService.findOne(TABLES.THREAD, { id: ensureRecordId(run.threadId, TABLES.THREAD) }, ThreadSchema),
292
- planRunService.listNodeSpecs(spec.id),
293
- planRunService.listNodeRuns(run.id),
210
+ const [thread, nodeSpecs, nodeRuns] = yield* Effect.all([
211
+ deps.db
212
+ .findOne(TABLES.THREAD, { id: ensureRecordId(run.threadId, TABLES.THREAD) }, ThreadSchema)
213
+ .pipe(Effect.mapError((cause) => toDispatchDatabaseError('Failed to load thread context.', cause))),
214
+ deps.planRun
215
+ .listNodeSpecs(spec.id)
216
+ .pipe(Effect.mapError((cause) => toDispatchDatabaseError('Failed to load plan node specs.', cause))),
217
+ deps.planRun
218
+ .listNodeRuns(run.id)
219
+ .pipe(Effect.mapError((cause) => toDispatchDatabaseError('Failed to load plan node runs.', cause))),
294
220
  ])
295
221
  const userId = thread?.userId ? recordIdToString(thread.userId, TABLES.USER) : undefined
296
- const userName = userId
297
- ? await userService
298
- .getUser(userId)
299
- .then((user) => user.name)
300
- .catch(() => undefined)
222
+ const userResult = userId ? yield* Effect.exit(deps.user.getUser(userId)) : undefined
223
+ const userName = userResult
224
+ ? Exit.match(userResult, { onSuccess: (user) => user.name, onFailure: () => undefined })
301
225
  : undefined
302
226
  const nodeSpecsById = new Map(nodeSpecs.map((candidate) => [candidate.nodeId, candidate]))
303
227
  const upstreamHandoffs: UpstreamHandoff[] = nodeRuns
@@ -324,19 +248,17 @@ class OwnershipDispatcherService {
324
248
  ...(userName ? { userName } : {}),
325
249
  ...(upstreamHandoffs.length > 0 ? { upstreamHandoffs } : {}),
326
250
  }
327
- }
251
+ })
328
252
 
329
- private async serializeRun(runId: RecordIdInput): Promise<SerializableExecutionPlan> {
330
- return planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
331
- includeEvents: true,
332
- includeArtifacts: true,
333
- includeApprovals: true,
334
- includeCheckpoints: true,
335
- includeValidationIssues: true,
336
- })
337
- }
253
+ const serializeRunEffect = (deps: OwnershipDispatcherDeps, runId: RecordIdInput) =>
254
+ Effect.gen(function* () {
255
+ const run = yield* deps.planRun.getRunById(runId)
256
+ return yield* serializeRunFull(deps.planRun, run)
257
+ })
338
258
 
339
- async dispatchNode(params: {
259
+ const dispatchNodeEffect = (
260
+ deps: OwnershipDispatcherDeps,
261
+ params: {
340
262
  nodeSpec: PlanNodeSpec
341
263
  resolvedInput: Record<string, unknown>
342
264
  inputArtifacts: PlanArtifactSubmission[]
@@ -344,83 +266,354 @@ class OwnershipDispatcherService {
344
266
  executionMode?: ExecutionMode
345
267
  executionModeOverride?: ExecutionMode
346
268
  schemaRegistry?: PlanSchemaRegistry
347
- }) {
269
+ },
270
+ ) =>
271
+ Effect.gen(function* () {
348
272
  const effectiveExecutionMode = params.executionModeOverride ?? params.executionMode
349
273
  if (params.nodeSpec.type === 'monitoring' && params.nodeSpec.monitoringConfig) {
350
- await monitoringWindowService.startMonitoringWindow({
274
+ const monitoringConfig = params.nodeSpec.monitoringConfig
275
+ yield* deps.monitoringWindow.startMonitoringWindow({
351
276
  runId: params.context.planId,
352
277
  nodeId: params.nodeSpec.id,
353
- config: params.nodeSpec.monitoringConfig,
278
+ config: monitoringConfig,
354
279
  organizationId: params.context.organizationId,
355
280
  threadId: params.context.threadId,
356
281
  })
357
282
  return {
358
283
  notes: `Monitoring window started for node "${params.nodeSpec.label}".`,
359
- structuredOutput: { status: 'monitoring-started', config: params.nodeSpec.monitoringConfig },
284
+ structuredOutput: { status: 'monitoring-started', config: monitoringConfig },
360
285
  artifacts: [],
361
286
  }
362
287
  }
363
288
 
364
- if (params.nodeSpec.owner.executorType === 'agent') {
365
- return agentExecutorService.executeNode({
366
- nodeSpec: params.nodeSpec,
367
- resolvedInput: params.resolvedInput,
368
- inputArtifacts: params.inputArtifacts,
369
- context: params.context,
370
- executionMode: effectiveExecutionMode,
371
- schemaRegistry: params.schemaRegistry,
372
- })
373
- }
374
- if (params.nodeSpec.owner.executorType === 'plugin') {
375
- return pluginExecutorService.executeNode({
376
- nodeSpec: params.nodeSpec,
377
- resolvedInput: params.resolvedInput,
378
- context: params.context,
379
- })
380
- }
381
- if (params.nodeSpec.owner.executorType === 'system') {
382
- return systemExecutorService.executeNode({
383
- nodeSpec: params.nodeSpec,
384
- resolvedInput: params.resolvedInput,
385
- context: params.context,
386
- })
387
- }
388
- if (params.nodeSpec.owner.executorType === 'skill') {
389
- const resolved = await skillResolverService.resolve({
390
- skillRef: params.nodeSpec.owner.ref,
391
- organizationId: params.context.organizationId,
392
- })
393
- if (!resolved) {
394
- throw new Error(`Skill "${params.nodeSpec.owner.ref}" could not be resolved. This is a configuration error.`)
395
- }
396
-
397
- if (resolved.executorType === 'agent') {
398
- const skillNodeSpec = { ...params.nodeSpec, owner: { executorType: 'agent' as const, ref: resolved.ref } }
399
- return agentExecutorService.executeNode({
400
- nodeSpec: skillNodeSpec,
289
+ return yield* Match.value(params.nodeSpec.owner.executorType).pipe(
290
+ Match.when('agent', () =>
291
+ deps.agentExecutor.executeNode({
292
+ nodeSpec: params.nodeSpec,
401
293
  resolvedInput: params.resolvedInput,
402
294
  inputArtifacts: params.inputArtifacts,
403
295
  context: params.context,
404
296
  executionMode: effectiveExecutionMode,
405
297
  schemaRegistry: params.schemaRegistry,
406
- })
298
+ }),
299
+ ),
300
+ Match.when('plugin', () =>
301
+ deps.pluginExecutor.executeNode({
302
+ nodeSpec: params.nodeSpec,
303
+ resolvedInput: params.resolvedInput,
304
+ context: params.context,
305
+ }),
306
+ ),
307
+ Match.when('system', () =>
308
+ deps.systemExecutor.executeNode({
309
+ nodeSpec: params.nodeSpec,
310
+ resolvedInput: params.resolvedInput,
311
+ context: params.context,
312
+ }),
313
+ ),
314
+ Match.when('skill', () =>
315
+ Effect.gen(function* () {
316
+ const resolved = yield* deps.skillResolver.resolve({
317
+ skillRef: params.nodeSpec.owner.ref,
318
+ organizationId: params.context.organizationId,
319
+ })
320
+ if (!resolved) {
321
+ return yield* new ConfigurationError({
322
+ message: `Skill "${params.nodeSpec.owner.ref}" could not be resolved. This is a configuration error.`,
323
+ })
324
+ }
325
+
326
+ if (resolved.executorType === 'agent') {
327
+ const skillNodeSpec = { ...params.nodeSpec, owner: { executorType: 'agent' as const, ref: resolved.ref } }
328
+ return yield* deps.agentExecutor.executeNode({
329
+ nodeSpec: skillNodeSpec,
330
+ resolvedInput: params.resolvedInput,
331
+ inputArtifacts: params.inputArtifacts,
332
+ context: params.context,
333
+ executionMode: effectiveExecutionMode,
334
+ schemaRegistry: params.schemaRegistry,
335
+ })
336
+ }
337
+
338
+ return yield* deps.pluginExecutor.executeNode({
339
+ nodeSpec: {
340
+ ...params.nodeSpec,
341
+ owner: {
342
+ executorType: 'plugin' as const,
343
+ ref: resolved.ref,
344
+ operation: resolved.operation ?? params.nodeSpec.owner.ref,
345
+ },
346
+ },
347
+ resolvedInput: params.resolvedInput,
348
+ context: params.context,
349
+ })
350
+ }),
351
+ ),
352
+ Match.when(
353
+ 'user',
354
+ () => new BadRequestError({ message: `User-owned node "${params.nodeSpec.id}" cannot be auto-dispatched.` }),
355
+ ),
356
+ Match.exhaustive,
357
+ )
358
+ })
359
+
360
+ type GraphFullDispatchReadyNode = (
361
+ params: Parameters<typeof dispatchReadyNodeEffect>[1],
362
+ ) => Effect.Effect<PlanNodeResultSubmission, Effect.Error<ReturnType<typeof dispatchReadyNodeEffect>>, never>
363
+
364
+ const dispatchRunToStableBoundaryEffect = (
365
+ deps: OwnershipDispatcherDeps,
366
+ params: { runId: RecordIdInput; emittedBy: string },
367
+ ) =>
368
+ Effect.gen(function* () {
369
+ const initialRun = yield* deps.planRun.getRunById(params.runId)
370
+ const autoDispatchEnabled = yield* shouldAutoDispatchEffect(deps, initialRun)
371
+ if (!autoDispatchEnabled) {
372
+ return yield* serializeRunEffect(deps, initialRun.id)
373
+ }
374
+
375
+ for (let iteration = 0; iteration < MAX_DISPATCH_ITERATIONS; iteration += 1) {
376
+ const run = yield* deps.planRun.getRunById(params.runId)
377
+ if (STABLE_RUN_STATUSES.has(run.status) || run.status !== 'running' || !run.currentNodeId) {
378
+ return yield* serializeRunEffect(deps, run.id)
407
379
  }
408
- return pluginExecutorService.executeNode({
409
- nodeSpec: {
410
- ...params.nodeSpec,
411
- owner: {
412
- executorType: 'plugin' as const,
413
- ref: resolved.ref,
414
- operation: resolved.operation ?? params.nodeSpec.owner.ref,
380
+
381
+ const spec = yield* deps.planRun.getPlanSpecById(run.planSpecId)
382
+
383
+ if (spec.executionMode === 'graph-full') {
384
+ const dispatchReadyNodeForGraphFull: GraphFullDispatchReadyNode = (dispatchParams) =>
385
+ dispatchReadyNodeEffect(deps, dispatchParams)
386
+
387
+ yield* routeGraphFullEffect(
388
+ { threadId: recordIdToString(run.threadId, TABLES.THREAD), runId: recordIdToString(run.id, TABLES.PLAN_RUN) },
389
+ {
390
+ dispatchReadyNode: dispatchReadyNodeForGraphFull,
391
+ planExecutorService: deps.planExecutor,
392
+ planRunService: deps.planRun,
415
393
  },
416
- },
417
- resolvedInput: params.resolvedInput,
418
- context: params.context,
394
+ ).pipe(
395
+ Effect.mapError(
396
+ () =>
397
+ new ConfigurationError({
398
+ message: `Failed to route graph-full execution for run ${recordIdToString(run.id, TABLES.PLAN_RUN)}.`,
399
+ }),
400
+ ),
401
+ )
402
+ return yield* serializeRunEffect(deps, run.id)
403
+ }
404
+
405
+ const currentNodeId = run.currentNodeId
406
+ if (!currentNodeId) {
407
+ return yield* serializeRunEffect(deps, run.id)
408
+ }
409
+
410
+ const nodeSpecRecord = yield* deps.planRun.getNodeSpecByNodeId(spec.id, currentNodeId)
411
+ const planNode = toPlanNodeSpec(nodeSpecRecord)
412
+ if (planNode.owner.executorType === 'user') {
413
+ return yield* serializeRunEffect(deps, run.id)
414
+ }
415
+
416
+ const nodeId = nodeSpecRecord.nodeId
417
+ if (!nodeId) {
418
+ return yield* new ConfigurationError({
419
+ message: `Node spec "${nodeSpecRecord.label}" is missing an executable node id.`,
420
+ })
421
+ }
422
+
423
+ const nodeRun = yield* deps.planRun.getNodeRunByNodeId(run.id, nodeId)
424
+ if (nodeRun.status === 'monitoring') {
425
+ return yield* serializeRunEffect(deps, run.id)
426
+ }
427
+ if (nodeRun.status !== 'running') {
428
+ return yield* serializeRunEffect(deps, run.id)
429
+ }
430
+ if (shouldPlanNodeUseVisibleTurn(spec, nodeSpecRecord)) {
431
+ return yield* serializeRunEffect(deps, run.id)
432
+ }
433
+
434
+ const [artifacts, dispatchContext] = yield* Effect.all([
435
+ deps.planRun
436
+ .listArtifacts(run.id)
437
+ .pipe(Effect.mapError((cause) => toDispatchDatabaseError('Failed to load run artifacts.', cause))),
438
+ buildDispatchContextEffect(deps, run, spec, nodeSpecRecord),
439
+ ])
440
+ const inputArtifacts = artifacts
441
+ .filter((artifact) => nodeSpecRecord.upstreamNodeIds.includes(artifact.nodeId))
442
+ .map((artifact) => toArtifactSubmission(artifact))
443
+
444
+ const dispatchExit = yield* Effect.exit(
445
+ Effect.gen(function* () {
446
+ const result = yield* dispatchNodeEffect(deps, {
447
+ nodeSpec: planNode,
448
+ resolvedInput: nodeRun.resolvedInput ?? {},
449
+ inputArtifacts,
450
+ context: { ...dispatchContext, nodeId: planNode.id },
451
+ executionMode: spec.executionMode,
452
+ schemaRegistry: spec.schemaRegistry,
453
+ })
454
+
455
+ yield* tryDispatchPromise(
456
+ () =>
457
+ deps.planExecutor.submitNodeResult({
458
+ threadId: run.threadId,
459
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
460
+ nodeId: planNode.id,
461
+ emittedBy: planNode.owner.ref,
462
+ result,
463
+ }),
464
+ 'Failed to submit plan node result.',
465
+ )
466
+ }),
467
+ )
468
+
469
+ if (Exit.isFailure(dispatchExit)) {
470
+ const failure = Cause.squash(dispatchExit.cause)
471
+ yield* tryDispatchPromise(
472
+ () =>
473
+ deps.planExecutor.blockNodeOnDispatchFailure({
474
+ threadId: run.threadId,
475
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
476
+ nodeId: planNode.id,
477
+ emittedBy: planNode.owner.ref,
478
+ message: formatDispatchError(failure),
479
+ failureClass: classifyDispatchFailure({ ownerType: planNode.owner.executorType, error: failure }),
480
+ }),
481
+ 'Failed to block plan node on dispatch failure.',
482
+ )
483
+ return yield* serializeRunEffect(deps, run.id)
484
+ }
485
+ }
486
+
487
+ return yield* new ConfigurationError({
488
+ message: `Ownership dispatch exceeded ${MAX_DISPATCH_ITERATIONS} iterations for run ${recordIdToString(
489
+ ensureRecordId(params.runId, TABLES.PLAN_RUN),
490
+ TABLES.PLAN_RUN,
491
+ )}.`,
492
+ })
493
+ })
494
+
495
+ const dispatchReadyNodeEffect = (
496
+ deps: OwnershipDispatcherDeps,
497
+ params: {
498
+ run: PlanRunRecord
499
+ nodeSpecRecord: PlanNodeSpecRecord
500
+ nodeRun: PlanNodeRunRecord
501
+ spec: PlanSpecRecord
502
+ executionModeOverride?: ExecutionMode
503
+ },
504
+ ) =>
505
+ Effect.gen(function* () {
506
+ if (shouldPlanNodeUseVisibleTurn(params.spec, params.nodeSpecRecord)) {
507
+ return yield* new BadRequestError({
508
+ message: `Node "${params.nodeSpecRecord.nodeId}" requires a visible plan turn and cannot be silently dispatched.`,
419
509
  })
420
510
  }
421
511
 
422
- throw new Error(`User-owned node "${params.nodeSpec.id}" cannot be auto-dispatched.`)
423
- }
512
+ const planNode = toPlanNodeSpec(params.nodeSpecRecord)
513
+ const [artifacts, dispatchContext] = yield* Effect.all([
514
+ deps.planRun
515
+ .listArtifacts(params.run.id)
516
+ .pipe(Effect.mapError((cause) => toDispatchDatabaseError('Failed to load run artifacts.', cause))),
517
+ buildDispatchContextEffect(deps, params.run, params.spec, params.nodeSpecRecord),
518
+ ])
519
+ const inputArtifacts = artifacts
520
+ .filter((artifact) => params.nodeSpecRecord.upstreamNodeIds.includes(artifact.nodeId))
521
+ .map((artifact) => toArtifactSubmission(artifact))
522
+
523
+ return yield* dispatchNodeEffect(deps, {
524
+ nodeSpec: planNode,
525
+ resolvedInput: params.nodeRun.resolvedInput ?? {},
526
+ inputArtifacts,
527
+ context: { ...dispatchContext, nodeId: planNode.id },
528
+ executionMode: params.spec.executionMode,
529
+ executionModeOverride: params.executionModeOverride,
530
+ schemaRegistry: params.spec.schemaRegistry,
531
+ })
532
+ })
533
+
534
+ function resolveExecutionVisibility(params: { spec: PlanSpecRecord; nodeSpecRecord: PlanNodeSpecRecord }) {
535
+ return resolvePlanNodeExecutionVisibility(params.spec, params.nodeSpecRecord)
424
536
  }
425
537
 
426
- export const ownershipDispatcherService = new OwnershipDispatcherService()
538
+ export function makeOwnershipDispatcherService(deps: OwnershipDispatcherDeps) {
539
+ return {
540
+ validateDraftExecutors(draft: PlanDraft) {
541
+ return validateDraftExecutors(deps, draft)
542
+ },
543
+ dispatchRunToStableBoundary(params: { runId: RecordIdInput; emittedBy: string }) {
544
+ return dispatchRunToStableBoundaryEffect(deps, params)
545
+ },
546
+ dispatchReadyNode(params: {
547
+ run: PlanRunRecord
548
+ nodeSpecRecord: PlanNodeSpecRecord
549
+ nodeRun: PlanNodeRunRecord
550
+ spec: PlanSpecRecord
551
+ executionModeOverride?: ExecutionMode
552
+ }) {
553
+ return dispatchReadyNodeEffect(deps, params)
554
+ },
555
+ resolveExecutionVisibility(params: { spec: PlanSpecRecord; nodeSpecRecord: PlanNodeSpecRecord }) {
556
+ return resolveExecutionVisibility(params)
557
+ },
558
+ dispatchNode(params: {
559
+ nodeSpec: PlanNodeSpec
560
+ resolvedInput: Record<string, unknown>
561
+ inputArtifacts: PlanArtifactSubmission[]
562
+ context: OwnershipDispatchContext
563
+ executionMode?: ExecutionMode
564
+ executionModeOverride?: ExecutionMode
565
+ schemaRegistry?: PlanSchemaRegistry
566
+ }) {
567
+ return dispatchNodeEffect(deps, params)
568
+ },
569
+ } as const
570
+ }
571
+
572
+ export interface OwnershipDispatcherService extends ReturnType<typeof makeOwnershipDispatcherService> {}
573
+
574
+ export class OwnershipDispatcherServiceTag extends Context.Service<
575
+ OwnershipDispatcherServiceTag,
576
+ OwnershipDispatcherService
577
+ >()('@lota-sdk/core/OwnershipDispatcherService') {}
578
+
579
+ export const OwnershipDispatcherServiceLive = Layer.effect(
580
+ OwnershipDispatcherServiceTag,
581
+ Effect.gen(function* () {
582
+ const currentContext = yield* Effect.context()
583
+ const provideCurrentContext = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, never> =>
584
+ effect.pipe(Effect.provide(currentContext)) as Effect.Effect<A, E, never>
585
+ const db = yield* DatabaseServiceTag
586
+ const agentConfig = yield* AgentConfigServiceTag
587
+ const runtimeAdapters = yield* RuntimeAdaptersServiceTag
588
+ const planRun = yield* PlanRunServiceTag
589
+ const agentExecutor = yield* AgentExecutorServiceTag
590
+ const monitoringWindow = yield* MonitoringWindowServiceTag
591
+ const planExecutor = yield* PlanExecutorServiceTag
592
+ const pluginExecutor = yield* PluginExecutorServiceTag
593
+ const skillResolver = yield* SkillResolverServiceTag
594
+ const systemExecutor = yield* SystemExecutorServiceTag
595
+ const user = yield* UserServiceTag
596
+ const executeAgentNode = agentExecutor.executeNode as OwnershipDispatcherDeps['agentExecutor']['executeNode']
597
+ return makeOwnershipDispatcherService({
598
+ db,
599
+ agentConfig,
600
+ runtimeAdapters,
601
+ agentExecutor: { executeNode: executeAgentNode },
602
+ monitoringWindow: {
603
+ startMonitoringWindow: (params) => provideCurrentContext(monitoringWindow.startMonitoringWindow(params)),
604
+ },
605
+ planExecutor,
606
+ planRun,
607
+ pluginExecutor: {
608
+ validateOwner: (owner, nodeId) => pluginExecutor.validateOwner(owner, nodeId),
609
+ executeNode: (params) => provideCurrentContext(pluginExecutor.executeNode(params)),
610
+ },
611
+ skillResolver: { resolve: (params) => provideCurrentContext(skillResolver.resolve(params)) },
612
+ systemExecutor: {
613
+ validateOwner: (owner, nodeId) => systemExecutor.validateOwner(owner, nodeId),
614
+ executeNode: (params) => provideCurrentContext(systemExecutor.executeNode(params)),
615
+ },
616
+ user: { getUser: (userId) => provideCurrentContext(user.getUser(userId)) },
617
+ })
618
+ }),
619
+ )