@lota-sdk/core 0.1.14 → 0.1.16

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 (174) hide show
  1. package/infrastructure/schema/00_identity.surql +0 -2
  2. package/infrastructure/schema/01_memory.surql +1 -1
  3. package/infrastructure/schema/02_execution_plan.surql +62 -1
  4. package/infrastructure/schema/03_learned_skill.surql +1 -1
  5. package/infrastructure/schema/06_playbook.surql +25 -0
  6. package/infrastructure/schema/07_institutional_memory.surql +13 -0
  7. package/infrastructure/schema/08_quality_metrics.surql +17 -0
  8. package/package.json +9 -8
  9. package/src/ai/definitions.ts +80 -2
  10. package/src/ai/embedding-cache.ts +7 -6
  11. package/src/ai/index.ts +0 -1
  12. package/src/bifrost/bifrost.ts +14 -14
  13. package/src/config/agent-defaults.ts +32 -22
  14. package/src/config/agent-types.ts +11 -0
  15. package/src/config/constants.ts +2 -14
  16. package/src/config/debug-logger.ts +5 -1
  17. package/src/config/index.ts +3 -0
  18. package/src/config/logger.ts +7 -9
  19. package/src/config/model-constants.ts +16 -34
  20. package/src/config/search.ts +1 -15
  21. package/src/create-runtime.ts +453 -0
  22. package/src/db/cursor-pagination.ts +3 -6
  23. package/src/db/index.ts +2 -0
  24. package/src/db/memory-store.rows.ts +7 -7
  25. package/src/db/memory-store.ts +24 -24
  26. package/src/db/memory.ts +18 -16
  27. package/src/db/schema-fingerprint.ts +1 -0
  28. package/src/db/service.ts +193 -122
  29. package/src/db/startup.ts +9 -13
  30. package/src/db/surreal-mutation.ts +43 -0
  31. package/src/db/tables.ts +7 -0
  32. package/src/db/workstream-message-row.ts +15 -0
  33. package/src/embeddings/provider.ts +1 -1
  34. package/src/index.ts +1 -1
  35. package/src/queues/context-compaction.queue.ts +17 -52
  36. package/src/queues/delayed-node-promotion.queue.ts +41 -0
  37. package/src/queues/document-processor.queue.ts +7 -7
  38. package/src/queues/index.ts +3 -0
  39. package/src/queues/memory-consolidation.queue.ts +18 -54
  40. package/src/queues/plan-scheduler.queue.ts +97 -0
  41. package/src/queues/post-chat-memory.queue.ts +15 -60
  42. package/src/queues/queue-factory.ts +100 -0
  43. package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
  44. package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
  45. package/src/queues/skill-extraction.queue.ts +15 -50
  46. package/src/queues/workstream-title-generation.queue.ts +15 -51
  47. package/src/redis/connection.ts +12 -3
  48. package/src/redis/index.ts +2 -1
  49. package/src/redis/org-memory-lock.ts +1 -1
  50. package/src/redis/redis-lease-lock.ts +41 -8
  51. package/src/redis/stream-context.ts +11 -0
  52. package/src/runtime/agent-runtime-policy.ts +106 -21
  53. package/src/runtime/agent-stream-helpers.ts +2 -1
  54. package/src/runtime/approval-continuation.ts +12 -6
  55. package/src/runtime/context-compaction-constants.ts +1 -1
  56. package/src/runtime/context-compaction-runtime.ts +7 -5
  57. package/src/runtime/context-compaction.ts +40 -97
  58. package/src/runtime/execution-plan.ts +23 -19
  59. package/src/runtime/graph-designer.ts +15 -0
  60. package/src/runtime/helper-model.ts +10 -196
  61. package/src/runtime/index.ts +14 -1
  62. package/src/runtime/llm-content.ts +1 -1
  63. package/src/runtime/memory-block.ts +11 -12
  64. package/src/runtime/memory-pipeline.ts +26 -10
  65. package/src/runtime/plugin-resolution.ts +35 -0
  66. package/src/runtime/plugin-types.ts +73 -1
  67. package/src/runtime/retrieval-adapters.ts +1 -1
  68. package/src/runtime/runtime-config.ts +25 -12
  69. package/src/runtime/runtime-extensions.ts +91 -15
  70. package/src/runtime/runtime-worker-registry.ts +6 -0
  71. package/src/runtime/team-consultation-orchestrator.ts +45 -28
  72. package/src/runtime/team-consultation-prompts.ts +11 -2
  73. package/src/runtime/title-helpers.ts +11 -4
  74. package/src/runtime/workstream-chat-helpers.ts +6 -7
  75. package/src/runtime/workstream-routing-policy.ts +0 -30
  76. package/src/runtime/workstream-state.ts +17 -7
  77. package/src/services/adaptive-playbook.service.ts +152 -0
  78. package/src/services/agent-executor.service.ts +293 -0
  79. package/src/services/artifact-provenance.service.ts +172 -0
  80. package/src/services/attachment.service.ts +7 -12
  81. package/src/services/context-compaction.service.ts +75 -58
  82. package/src/services/context-enrichment.service.ts +33 -0
  83. package/src/services/coordination-registry.service.ts +117 -0
  84. package/src/services/document-chunk.service.ts +38 -33
  85. package/src/services/domain-agent-executor.service.ts +71 -0
  86. package/src/services/execution-plan.service.ts +271 -50
  87. package/src/services/feedback-loop.service.ts +96 -0
  88. package/src/services/global-orchestrator.service.ts +148 -0
  89. package/src/services/index.ts +26 -0
  90. package/src/services/institutional-memory.service.ts +145 -0
  91. package/src/services/learned-skill.service.ts +30 -15
  92. package/src/services/memory-assessment.service.ts +3 -2
  93. package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
  94. package/src/services/memory.service.ts +55 -69
  95. package/src/services/monitoring-window.service.ts +86 -0
  96. package/src/services/mutating-approval.service.ts +1 -1
  97. package/src/services/node-workspace.service.ts +155 -0
  98. package/src/services/notification.service.ts +39 -0
  99. package/src/services/organization-member.service.ts +12 -5
  100. package/src/services/organization.service.ts +5 -5
  101. package/src/services/ownership-dispatcher.service.ts +403 -0
  102. package/src/services/plan-approval.service.ts +1 -1
  103. package/src/services/plan-artifact.service.ts +1 -0
  104. package/src/services/plan-builder.service.ts +1 -0
  105. package/src/services/plan-checkpoint.service.ts +30 -2
  106. package/src/services/plan-compiler.service.ts +5 -0
  107. package/src/services/plan-coordination.service.ts +152 -0
  108. package/src/services/plan-cycle.service.ts +284 -0
  109. package/src/services/plan-deadline.service.ts +287 -0
  110. package/src/services/plan-executor.service.ts +386 -58
  111. package/src/services/plan-helpers.ts +15 -0
  112. package/src/services/plan-run.service.ts +41 -7
  113. package/src/services/plan-scheduler.service.ts +240 -0
  114. package/src/services/plan-template.service.ts +117 -0
  115. package/src/services/plan-validator.service.ts +87 -20
  116. package/src/services/plan-workspace.service.ts +83 -0
  117. package/src/services/playbook-registry.service.ts +67 -0
  118. package/src/services/plugin-executor.service.ts +103 -0
  119. package/src/services/quality-metrics.service.ts +132 -0
  120. package/src/services/recent-activity-title.service.ts +3 -10
  121. package/src/services/recent-activity.service.ts +33 -43
  122. package/src/services/skill-resolver.service.ts +19 -0
  123. package/src/services/system-executor.service.ts +105 -0
  124. package/src/services/workstream-message.service.ts +29 -41
  125. package/src/services/workstream-plan-registry.service.ts +22 -0
  126. package/src/services/workstream-title.service.ts +3 -9
  127. package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
  128. package/src/services/workstream-turn.ts +2 -2
  129. package/src/services/workstream.service.ts +55 -65
  130. package/src/services/workstream.types.ts +10 -19
  131. package/src/services/write-intent-validator.service.ts +81 -0
  132. package/src/storage/attachment-parser.ts +1 -1
  133. package/src/storage/attachment-storage.service.ts +4 -4
  134. package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
  135. package/src/storage/generated-document-storage.service.ts +3 -2
  136. package/src/storage/index.ts +2 -2
  137. package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
  138. package/src/system-agents/delegated-agent-factory.ts +5 -2
  139. package/src/system-agents/index.ts +8 -0
  140. package/src/system-agents/memory-reranker.agent.ts +1 -1
  141. package/src/system-agents/memory.agent.ts +1 -1
  142. package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
  143. package/src/tools/execution-plan.tool.ts +17 -19
  144. package/src/tools/fetch-webpage.tool.ts +20 -18
  145. package/src/tools/index.ts +2 -3
  146. package/src/tools/read-file-parts.tool.ts +1 -1
  147. package/src/tools/search-web.tool.ts +18 -15
  148. package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
  149. package/src/tools/team-think.tool.ts +14 -8
  150. package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
  151. package/src/utils/async.ts +3 -2
  152. package/src/utils/date-time.ts +4 -32
  153. package/src/utils/env.ts +8 -0
  154. package/src/utils/errors.ts +47 -0
  155. package/src/utils/hono-error-handler.ts +1 -2
  156. package/src/utils/index.ts +19 -2
  157. package/src/utils/string.ts +128 -1
  158. package/src/workers/bootstrap.ts +2 -2
  159. package/src/workers/index.ts +1 -0
  160. package/src/workers/memory-consolidation.worker.ts +12 -12
  161. package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
  162. package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
  163. package/src/workers/skill-extraction.runner.ts +8 -102
  164. package/src/workers/utils/file-section-chunker.ts +6 -3
  165. package/src/workers/utils/repomix-file-sections.ts +2 -2
  166. package/src/workers/utils/sandbox-error.ts +11 -2
  167. package/src/workers/utils/workstream-message-query.ts +97 -0
  168. package/src/workers/worker-utils.ts +6 -2
  169. package/src/runtime/retrieval-pipeline.ts +0 -3
  170. package/src/runtime.ts +0 -387
  171. package/src/tools/log-hello-world.tool.ts +0 -17
  172. package/src/utils/error.ts +0 -10
  173. /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
  174. /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
@@ -0,0 +1,403 @@
1
+ import type {
2
+ ExecutionMode,
3
+ OwnershipDispatchContext,
4
+ PlanArtifactRecord,
5
+ PlanArtifactSubmission,
6
+ PlanFailureClass,
7
+ PlanNodeResultSubmission,
8
+ PlanNodeRunRecord,
9
+ PlanNodeSpec,
10
+ PlanNodeSpecRecord,
11
+ PlanRunRecord,
12
+ PlanSchemaRegistry,
13
+ PlanSpecRecord,
14
+ PlanDraft,
15
+ SerializableExecutionPlan,
16
+ } from '@lota-sdk/shared'
17
+
18
+ import { agentRoster } from '../config/agent-defaults'
19
+ import type { RecordIdInput } from '../db/record-id'
20
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
21
+ import { databaseService } from '../db/service'
22
+ import { TABLES } from '../db/tables'
23
+ import { getRuntimeAdapters } from '../runtime/runtime-extensions'
24
+ import { agentExecutorService } from './agent-executor.service'
25
+ import { domainAgentExecutorService } from './domain-agent-executor.service'
26
+ import { monitoringWindowService } from './monitoring-window.service'
27
+ import { planExecutorService } from './plan-executor.service'
28
+ import { planRunService } from './plan-run.service'
29
+ import type { PlanValidationIssueInput } from './plan-validator.service'
30
+ import { pluginExecutorService } from './plugin-executor.service'
31
+ import { skillResolverService } from './skill-resolver.service'
32
+ import { systemExecutorService } from './system-executor.service'
33
+ import { userService } from './user.service'
34
+ import { WorkstreamSchema } from './workstream.types'
35
+
36
+ const STABLE_RUN_STATUSES = new Set(['awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
37
+ const MAX_DISPATCH_ITERATIONS = 64
38
+
39
+ function toPlanNodeSpec(nodeSpec: PlanNodeSpecRecord): PlanNodeSpec {
40
+ return {
41
+ id: nodeSpec.nodeId,
42
+ type: nodeSpec.type,
43
+ label: nodeSpec.label,
44
+ owner: nodeSpec.owner,
45
+ objective: nodeSpec.objective,
46
+ instructions: nodeSpec.instructions,
47
+ inputSchemaRef: nodeSpec.inputSchemaRef,
48
+ outputSchemaRef: nodeSpec.outputSchemaRef,
49
+ deliverables: [...nodeSpec.deliverables],
50
+ successCriteria: [...nodeSpec.successCriteria],
51
+ completionChecks: [...nodeSpec.completionChecks],
52
+ retryPolicy: { ...nodeSpec.retryPolicy, retryOn: [...nodeSpec.retryPolicy.retryOn] },
53
+ failurePolicy: [...nodeSpec.failurePolicy],
54
+ timeoutMs: nodeSpec.timeoutMs,
55
+ toolPolicy: { allow: [...nodeSpec.toolPolicy.allow], deny: [...nodeSpec.toolPolicy.deny] },
56
+ contextPolicy: {
57
+ retrievalScopes: [...nodeSpec.contextPolicy.retrievalScopes],
58
+ attachmentPolicy: nodeSpec.contextPolicy.attachmentPolicy,
59
+ webPolicy: nodeSpec.contextPolicy.webPolicy,
60
+ },
61
+ ...(nodeSpec.schedule ? { schedule: nodeSpec.schedule } : {}),
62
+ ...(nodeSpec.deadline ? { deadline: nodeSpec.deadline } : {}),
63
+ ...(nodeSpec.monitoringConfig ? { monitoringConfig: nodeSpec.monitoringConfig } : {}),
64
+ ...(nodeSpec.delayAfterPredecessorMs ? { delayAfterPredecessorMs: nodeSpec.delayAfterPredecessorMs } : {}),
65
+ ...(nodeSpec.deliberationConfig ? { deliberationConfig: nodeSpec.deliberationConfig } : {}),
66
+ }
67
+ }
68
+
69
+ function toArtifactSubmission(artifact: PlanArtifactRecord): PlanArtifactSubmission {
70
+ return {
71
+ name: artifact.name,
72
+ kind: artifact.kind,
73
+ pointer: artifact.pointer,
74
+ ...(artifact.schemaRef ? { schemaRef: artifact.schemaRef } : {}),
75
+ ...(artifact.description ? { description: artifact.description } : {}),
76
+ ...(artifact.payload !== undefined ? { payload: artifact.payload } : {}),
77
+ }
78
+ }
79
+
80
+ function classifyDispatchFailure(params: {
81
+ ownerType: PlanNodeSpec['owner']['executorType']
82
+ error: unknown
83
+ }): PlanFailureClass {
84
+ const errorMessage =
85
+ params.error instanceof Error ? params.error.message.toLowerCase() : String(params.error).toLowerCase()
86
+ if (errorMessage.includes('timeout')) {
87
+ return 'timeout_exceeded'
88
+ }
89
+ if (params.ownerType === 'plugin' || params.ownerType === 'system') {
90
+ return 'external_system_unavailable'
91
+ }
92
+ return 'non_recoverable_logic_error'
93
+ }
94
+
95
+ function formatDispatchError(error: unknown): string {
96
+ return error instanceof Error ? error.message : String(error)
97
+ }
98
+
99
+ class OwnershipDispatcherService {
100
+ validateDraftExecutors(draft: PlanDraft): PlanValidationIssueInput[] {
101
+ const issues: PlanValidationIssueInput[] = []
102
+
103
+ for (const node of draft.nodes) {
104
+ if (node.owner.executorType === 'agent') {
105
+ if (!agentRoster.includes(node.owner.ref) && !domainAgentExecutorService.hasAgent(node.owner.ref)) {
106
+ issues.push({
107
+ severity: 'blocking',
108
+ code: 'agent_executor_missing',
109
+ message: `Node "${node.label}" references unknown agent executor "${node.owner.ref}".`,
110
+ nodeId: node.id,
111
+ detail: { agentId: node.owner.ref },
112
+ })
113
+ }
114
+ continue
115
+ }
116
+
117
+ if (node.owner.executorType === 'plugin') {
118
+ issues.push(...pluginExecutorService.validateOwner(node.owner, node.id))
119
+ continue
120
+ }
121
+
122
+ if (node.owner.executorType === 'system') {
123
+ issues.push(...systemExecutorService.validateOwner(node.owner, node.id))
124
+ continue
125
+ }
126
+
127
+ if (node.owner.executorType === 'skill') {
128
+ // Skill owners are validated at execution time via skillResolverService.
129
+ }
130
+ }
131
+
132
+ return issues
133
+ }
134
+
135
+ async dispatchRunToStableBoundary(params: {
136
+ runId: RecordIdInput
137
+ emittedBy: string
138
+ }): Promise<SerializableExecutionPlan> {
139
+ const initialRun = await planRunService.getRunById(params.runId)
140
+ const autoDispatchEnabled = await this.shouldAutoDispatch(initialRun)
141
+ if (!autoDispatchEnabled) {
142
+ return this.serializeRun(initialRun.id)
143
+ }
144
+
145
+ let iteration = 0
146
+ while (iteration < MAX_DISPATCH_ITERATIONS) {
147
+ const run = await planRunService.getRunById(params.runId)
148
+ if (STABLE_RUN_STATUSES.has(run.status) || run.status !== 'running' || !run.currentNodeId) {
149
+ return this.serializeRun(run.id)
150
+ }
151
+
152
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
153
+
154
+ if (spec.executionMode === 'graph-full') {
155
+ const { globalOrchestratorService } = await import('./global-orchestrator.service')
156
+ await globalOrchestratorService.routeGraphFull({
157
+ workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
158
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
159
+ })
160
+ return this.serializeRun(run.id)
161
+ }
162
+
163
+ const nodeSpecRecord = await planRunService.getNodeSpecByNodeId(spec.id, run.currentNodeId)
164
+ const planNode = toPlanNodeSpec(nodeSpecRecord)
165
+ if (planNode.owner.executorType === 'user') {
166
+ return this.serializeRun(run.id)
167
+ }
168
+
169
+ const nodeRun = await planRunService.getNodeRunByNodeId(run.id, nodeSpecRecord.nodeId)
170
+ if (nodeRun.status === 'monitoring') {
171
+ // Monitoring nodes are managed by the scheduler — treat as stable
172
+ return this.serializeRun(run.id)
173
+ }
174
+ if (nodeRun.status !== 'running') {
175
+ return this.serializeRun(run.id)
176
+ }
177
+
178
+ const [artifacts, dispatchContext] = await Promise.all([
179
+ planRunService.listArtifacts(run.id),
180
+ this.buildDispatchContext(run),
181
+ ])
182
+ const inputArtifacts = artifacts
183
+ .filter((artifact) => nodeSpecRecord.upstreamNodeIds.includes(artifact.nodeId))
184
+ .map((artifact) => toArtifactSubmission(artifact))
185
+
186
+ try {
187
+ const result = await this.dispatchNode({
188
+ nodeSpec: planNode,
189
+ resolvedInput: nodeRun.resolvedInput ?? {},
190
+ inputArtifacts,
191
+ context: { ...dispatchContext, nodeId: planNode.id },
192
+ executionMode: spec.executionMode,
193
+ schemaRegistry: spec.schemaRegistry,
194
+ })
195
+
196
+ await planExecutorService.submitNodeResult({
197
+ workstreamId: run.workstreamId,
198
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
199
+ nodeId: planNode.id,
200
+ emittedBy: planNode.owner.ref,
201
+ result,
202
+ })
203
+ } catch (error) {
204
+ await planExecutorService.blockNodeOnDispatchFailure({
205
+ workstreamId: run.workstreamId,
206
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
207
+ nodeId: planNode.id,
208
+ emittedBy: planNode.owner.ref,
209
+ message: formatDispatchError(error),
210
+ failureClass: classifyDispatchFailure({ ownerType: planNode.owner.executorType, error }),
211
+ })
212
+ return await this.serializeRun(run.id)
213
+ }
214
+
215
+ iteration += 1
216
+ }
217
+
218
+ throw new Error(
219
+ `Ownership dispatch exceeded ${MAX_DISPATCH_ITERATIONS} iterations for run ${recordIdToString(
220
+ ensureRecordId(params.runId, TABLES.PLAN_RUN),
221
+ TABLES.PLAN_RUN,
222
+ )}.`,
223
+ )
224
+ }
225
+
226
+ async dispatchReadyNode(params: {
227
+ run: PlanRunRecord
228
+ nodeSpecRecord: PlanNodeSpecRecord
229
+ nodeRun: PlanNodeRunRecord
230
+ spec: PlanSpecRecord
231
+ executionModeOverride?: ExecutionMode
232
+ }): Promise<PlanNodeResultSubmission> {
233
+ const planNode = toPlanNodeSpec(params.nodeSpecRecord)
234
+ const [artifacts, dispatchContext] = await Promise.all([
235
+ planRunService.listArtifacts(params.run.id),
236
+ this.buildDispatchContext(params.run),
237
+ ])
238
+ const inputArtifacts = artifacts
239
+ .filter((artifact) => params.nodeSpecRecord.upstreamNodeIds.includes(artifact.nodeId))
240
+ .map((artifact) => toArtifactSubmission(artifact))
241
+
242
+ return this.dispatchNode({
243
+ nodeSpec: planNode,
244
+ resolvedInput: params.nodeRun.resolvedInput ?? {},
245
+ inputArtifacts,
246
+ context: { ...dispatchContext, nodeId: planNode.id },
247
+ executionMode: params.spec.executionMode,
248
+ executionModeOverride: params.executionModeOverride,
249
+ schemaRegistry: params.spec.schemaRegistry,
250
+ })
251
+ }
252
+
253
+ private async shouldAutoDispatch(run: PlanRunRecord): Promise<boolean> {
254
+ const workspaceProvider = getRuntimeAdapters().services?.workspaceProvider
255
+ if (!workspaceProvider) {
256
+ return true
257
+ }
258
+
259
+ const workspace = await workspaceProvider.getWorkspace(ensureRecordId(run.organizationId, TABLES.ORGANIZATION))
260
+ const lifecycleState = await workspaceProvider.getLifecycleState?.(workspace)
261
+ return lifecycleState?.bootstrapActive !== true
262
+ }
263
+
264
+ private async buildDispatchContext(run: PlanRunRecord): Promise<Omit<OwnershipDispatchContext, 'nodeId'>> {
265
+ const organizationId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
266
+ const workstreamId = recordIdToString(run.workstreamId, TABLES.WORKSTREAM)
267
+ const planId = recordIdToString(run.id, TABLES.PLAN_RUN)
268
+ const workstream = await databaseService.findOne(
269
+ TABLES.WORKSTREAM,
270
+ { id: ensureRecordId(run.workstreamId, TABLES.WORKSTREAM) },
271
+ WorkstreamSchema,
272
+ )
273
+ const userId = workstream?.userId ? recordIdToString(workstream.userId, TABLES.USER) : undefined
274
+ const userName = userId
275
+ ? await userService
276
+ .getUser(userId)
277
+ .then((user) => user.name)
278
+ .catch(() => undefined)
279
+ : undefined
280
+
281
+ return {
282
+ organizationId,
283
+ workstreamId,
284
+ planId,
285
+ leadAgentId: run.leadAgentId,
286
+ ...(userId ? { userId } : {}),
287
+ ...(userName ? { userName } : {}),
288
+ }
289
+ }
290
+
291
+ private async serializeRun(runId: RecordIdInput): Promise<SerializableExecutionPlan> {
292
+ return planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
293
+ includeEvents: true,
294
+ includeArtifacts: true,
295
+ includeApprovals: true,
296
+ includeCheckpoints: true,
297
+ includeValidationIssues: true,
298
+ })
299
+ }
300
+
301
+ async dispatchNode(params: {
302
+ nodeSpec: PlanNodeSpec
303
+ resolvedInput: Record<string, unknown>
304
+ inputArtifacts: PlanArtifactSubmission[]
305
+ context: OwnershipDispatchContext
306
+ executionMode?: ExecutionMode
307
+ executionModeOverride?: ExecutionMode
308
+ schemaRegistry?: PlanSchemaRegistry
309
+ }) {
310
+ const effectiveExecutionMode = params.executionModeOverride ?? params.executionMode
311
+ if (params.nodeSpec.type === 'monitoring' && params.nodeSpec.monitoringConfig) {
312
+ await monitoringWindowService.startMonitoringWindow({
313
+ runId: params.context.planId,
314
+ nodeId: params.nodeSpec.id,
315
+ config: params.nodeSpec.monitoringConfig,
316
+ organizationId: params.context.organizationId,
317
+ workstreamId: params.context.workstreamId,
318
+ })
319
+ return {
320
+ structuredOutput: { status: 'monitoring-started', config: params.nodeSpec.monitoringConfig },
321
+ artifacts: [],
322
+ }
323
+ }
324
+
325
+ if (params.nodeSpec.owner.executorType === 'agent') {
326
+ if (domainAgentExecutorService.hasAgent(params.nodeSpec.owner.ref)) {
327
+ return domainAgentExecutorService.executeNode({
328
+ nodeSpec: params.nodeSpec,
329
+ resolvedInput: params.resolvedInput,
330
+ inputArtifacts: params.inputArtifacts,
331
+ context: params.context,
332
+ })
333
+ }
334
+ return agentExecutorService.executeNode({
335
+ nodeSpec: params.nodeSpec,
336
+ resolvedInput: params.resolvedInput,
337
+ inputArtifacts: params.inputArtifacts,
338
+ context: params.context,
339
+ executionMode: effectiveExecutionMode,
340
+ schemaRegistry: params.schemaRegistry,
341
+ })
342
+ }
343
+ if (params.nodeSpec.owner.executorType === 'plugin') {
344
+ return pluginExecutorService.executeNode({
345
+ nodeSpec: params.nodeSpec,
346
+ resolvedInput: params.resolvedInput,
347
+ context: params.context,
348
+ })
349
+ }
350
+ if (params.nodeSpec.owner.executorType === 'system') {
351
+ return systemExecutorService.executeNode({
352
+ nodeSpec: params.nodeSpec,
353
+ resolvedInput: params.resolvedInput,
354
+ context: params.context,
355
+ })
356
+ }
357
+ if (params.nodeSpec.owner.executorType === 'skill') {
358
+ const resolved = await skillResolverService.resolve({
359
+ skillRef: params.nodeSpec.owner.ref,
360
+ organizationId: params.context.organizationId,
361
+ })
362
+ if (!resolved) {
363
+ throw new Error(`Skill "${params.nodeSpec.owner.ref}" could not be resolved. This is a configuration error.`)
364
+ }
365
+
366
+ if (resolved.executorType === 'agent') {
367
+ const skillNodeSpec = { ...params.nodeSpec, owner: { executorType: 'agent' as const, ref: resolved.ref } }
368
+ if (domainAgentExecutorService.hasAgent(resolved.ref)) {
369
+ return domainAgentExecutorService.executeNode({
370
+ nodeSpec: skillNodeSpec,
371
+ resolvedInput: params.resolvedInput,
372
+ inputArtifacts: params.inputArtifacts,
373
+ context: params.context,
374
+ })
375
+ }
376
+ return agentExecutorService.executeNode({
377
+ nodeSpec: skillNodeSpec,
378
+ resolvedInput: params.resolvedInput,
379
+ inputArtifacts: params.inputArtifacts,
380
+ context: params.context,
381
+ executionMode: effectiveExecutionMode,
382
+ schemaRegistry: params.schemaRegistry,
383
+ })
384
+ }
385
+ return pluginExecutorService.executeNode({
386
+ nodeSpec: {
387
+ ...params.nodeSpec,
388
+ owner: {
389
+ executorType: 'plugin' as const,
390
+ ref: resolved.ref,
391
+ operation: resolved.operation ?? params.nodeSpec.owner.ref,
392
+ },
393
+ },
394
+ resolvedInput: params.resolvedInput,
395
+ context: params.context,
396
+ })
397
+ }
398
+
399
+ throw new Error(`User-owned node "${params.nodeSpec.id}" cannot be auto-dispatched.`)
400
+ }
401
+ }
402
+
403
+ export const ownershipDispatcherService = new OwnershipDispatcherService()
@@ -35,7 +35,7 @@ class PlanApprovalService {
35
35
  }
36
36
 
37
37
  async getApprovalById(approvalId: RecordIdInput): Promise<PlanApprovalRecord | null> {
38
- return await databaseService.findOne(
38
+ return databaseService.findOne(
39
39
  TABLES.PLAN_APPROVAL,
40
40
  { id: ensureRecordId(approvalId, TABLES.PLAN_APPROVAL) },
41
41
  PlanApprovalSchema,
@@ -17,6 +17,7 @@ class PlanArtifactService {
17
17
  }): Promise<PlanArtifactRecord[]> {
18
18
  const records: PlanArtifactRecord[] = []
19
19
 
20
+ // Sequential: SurrealDB transactions require ordered operations
20
21
  for (const artifact of params.artifacts) {
21
22
  const artifactId = new RecordId(TABLES.PLAN_ARTIFACT, Bun.randomUUIDv7())
22
23
  const created = await params.tx
@@ -23,6 +23,7 @@ class PlanBuilderService {
23
23
  structureDesign(draft: PlanDraft): PlanDraft {
24
24
  return {
25
25
  ...draft,
26
+ executionMode: draft.executionMode ?? 'linear',
26
27
  edges: buildImplicitLinearEdges(draft),
27
28
  entryNodeIds: draft.entryNodeIds && draft.entryNodeIds.length > 0 ? draft.entryNodeIds : [draft.nodes[0].id],
28
29
  }
@@ -2,11 +2,13 @@ import { PlanCheckpointSchema } from '@lota-sdk/shared'
2
2
  import type { PlanCheckpointRecord, PlanRunStatus } from '@lota-sdk/shared'
3
3
  import { RecordId } from 'surrealdb'
4
4
 
5
+ import { serverLogger } from '../config/logger'
5
6
  import type { RecordIdInput } from '../db/record-id'
6
- import { ensureRecordId } from '../db/record-id'
7
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
7
8
  import { databaseService } from '../db/service'
8
9
  import type { DatabaseTransaction } from '../db/service'
9
10
  import { TABLES } from '../db/tables'
11
+ import { planWorkspaceService } from './plan-workspace.service'
10
12
 
11
13
  class PlanCheckpointService {
12
14
  async createCheckpoint(params: {
@@ -19,7 +21,33 @@ class PlanCheckpointService {
19
21
  artifactIds: RecordIdInput[]
20
22
  lastCompletedNodeIds: string[]
21
23
  snapshot: Record<string, unknown>
24
+ includeWorkspace?: boolean
22
25
  }): Promise<PlanCheckpointRecord> {
26
+ const snapshotData = { ...params.snapshot }
27
+
28
+ if (params.includeWorkspace) {
29
+ const runIdStr = recordIdToString(params.runId, TABLES.PLAN_RUN)
30
+ const workspaceSnapshot = await planWorkspaceService
31
+ .currentRead({ runId: runIdStr })
32
+ .then((all) => {
33
+ // Filter to entries at or before the checkpoint sequence
34
+ const filtered: Record<string, unknown> = {}
35
+ for (const [key, entry] of Object.entries(all)) {
36
+ if (entry.writeSequence <= params.sequence) {
37
+ filtered[key] = entry
38
+ }
39
+ }
40
+ return filtered
41
+ })
42
+ .catch((error) => {
43
+ serverLogger.warn`Workspace snapshot failed for run ${runIdStr}: ${error}`
44
+ return {}
45
+ })
46
+ if (Object.keys(workspaceSnapshot).length > 0) {
47
+ snapshotData.workspaceSnapshot = workspaceSnapshot
48
+ }
49
+ }
50
+
23
51
  const checkpointId = new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7())
24
52
  const created = await params.tx
25
53
  .create(checkpointId)
@@ -31,7 +59,7 @@ class PlanCheckpointService {
31
59
  activeNodeIds: [...params.activeNodeIds],
32
60
  artifactIds: params.artifactIds.map((artifactId) => ensureRecordId(artifactId, TABLES.PLAN_ARTIFACT)),
33
61
  lastCompletedNodeIds: [...params.lastCompletedNodeIds],
34
- snapshot: params.snapshot,
62
+ snapshot: snapshotData,
35
63
  })
36
64
  .output('after')
37
65
 
@@ -72,6 +72,11 @@ class PlanCompilerService {
72
72
  attachmentPolicy: node.contextPolicy.attachmentPolicy,
73
73
  webPolicy: node.contextPolicy.webPolicy,
74
74
  },
75
+ ...(node.schedule ? { schedule: node.schedule } : {}),
76
+ ...(node.deadline ? { deadline: node.deadline } : {}),
77
+ ...(node.monitoringConfig ? { monitoringConfig: node.monitoringConfig } : {}),
78
+ ...(node.delayAfterPredecessorMs ? { delayAfterPredecessorMs: node.delayAfterPredecessorMs } : {}),
79
+ ...(node.deliberationConfig ? { deliberationConfig: node.deliberationConfig } : {}),
75
80
  upstreamNodeIds,
76
81
  downstreamNodeIds,
77
82
  }))
@@ -0,0 +1,152 @@
1
+ import type { PlanArtifactRecord, PlanDependency } from '@lota-sdk/shared'
2
+
3
+ import { serverLogger } from '../config/logger'
4
+ import type { PlanValidationIssueInput } from './plan-validator.service'
5
+
6
+ export interface DependencyResolutionResult {
7
+ resolved: Map<string, PlanArtifactRecord>
8
+ unresolved: PlanDependency[]
9
+ notifications: Array<{ dependency: PlanDependency; reason: string }>
10
+ }
11
+
12
+ class PlanCoordinationService {
13
+ /**
14
+ * Resolve cross-plan artifact dependencies.
15
+ * For each dependency:
16
+ * 1. Find the source plan by title in the workstream
17
+ * 2. Find the artifact by (nodeId, artifactName) in that plan's run
18
+ * 3. Check staleness if maxStalenessMs set
19
+ * 4. Based on triggerMode:
20
+ * 'block' -> unresolved if missing/stale
21
+ * 'notify' -> proceed but record notification
22
+ * 'best-effort' -> proceed regardless, no notification
23
+ */
24
+ async resolveDependencies(params: {
25
+ dependencies: PlanDependency[]
26
+ workstreamId: string
27
+ }): Promise<DependencyResolutionResult> {
28
+ const { planRunService } = await import('./plan-run.service')
29
+
30
+ const resolved = new Map<string, PlanArtifactRecord>()
31
+ const unresolved: PlanDependency[] = []
32
+ const notifications: DependencyResolutionResult['notifications'] = []
33
+
34
+ for (const dep of params.dependencies) {
35
+ const depKey = `${dep.sourcePlanTitle}:${dep.sourceNodeId}:${dep.artifactName}`
36
+
37
+ const specs = await planRunService.listPlanSpecsByWorkstream(params.workstreamId)
38
+ const sourceSpec = specs.find((s) => s.title === dep.sourcePlanTitle)
39
+ if (!sourceSpec) {
40
+ const reason = `Source plan "${dep.sourcePlanTitle}" not found in workstream.`
41
+ if (dep.triggerMode === 'block') {
42
+ unresolved.push(dep)
43
+ } else if (dep.triggerMode === 'notify') {
44
+ notifications.push({ dependency: dep, reason })
45
+ serverLogger.warn`Dependency unmet (notify): ${reason}`
46
+ }
47
+ // best-effort: silently proceed
48
+ continue
49
+ }
50
+
51
+ const runs = await planRunService.listRunsBySpec(sourceSpec.id)
52
+ const activeRun = runs.find((r) => r.status === 'completed' || r.status === 'running')
53
+ if (!activeRun) {
54
+ const reason = `No active run found for plan "${dep.sourcePlanTitle}".`
55
+ if (dep.triggerMode === 'block') {
56
+ unresolved.push(dep)
57
+ } else if (dep.triggerMode === 'notify') {
58
+ notifications.push({ dependency: dep, reason })
59
+ serverLogger.warn`Dependency unmet (notify): ${reason}`
60
+ }
61
+ continue
62
+ }
63
+
64
+ const artifacts = await planRunService.listArtifacts(activeRun.id)
65
+ const artifact = artifacts.find((a) => a.nodeId === dep.sourceNodeId && a.name === dep.artifactName)
66
+
67
+ if (!artifact) {
68
+ const reason = `Artifact "${dep.artifactName}" not found on node "${dep.sourceNodeId}" in plan "${dep.sourcePlanTitle}".`
69
+ if (dep.triggerMode === 'block') {
70
+ unresolved.push(dep)
71
+ } else if (dep.triggerMode === 'notify') {
72
+ notifications.push({ dependency: dep, reason })
73
+ serverLogger.warn`Dependency unmet (notify): ${reason}`
74
+ }
75
+ continue
76
+ }
77
+
78
+ if (dep.maxStalenessMs && this.isStale(artifact, dep.maxStalenessMs)) {
79
+ const reason = `Artifact "${dep.artifactName}" from plan "${dep.sourcePlanTitle}" is stale.`
80
+ if (dep.triggerMode === 'block') {
81
+ unresolved.push(dep)
82
+ continue
83
+ }
84
+ if (dep.triggerMode === 'notify') {
85
+ notifications.push({ dependency: dep, reason })
86
+ serverLogger.warn`Dependency stale (notify): ${reason}`
87
+ }
88
+ // best-effort and notify: use stale artifact anyway
89
+ }
90
+
91
+ resolved.set(depKey, artifact)
92
+ }
93
+
94
+ return { resolved, unresolved, notifications }
95
+ }
96
+
97
+ /** Check if an artifact has exceeded the staleness window. */
98
+ isStale(artifact: Pick<PlanArtifactRecord, 'createdAt'>, maxStalenessMs: number): boolean {
99
+ if (!maxStalenessMs) return false
100
+ return Date.now() - artifact.createdAt.getTime() > maxStalenessMs
101
+ }
102
+
103
+ /**
104
+ * Validate no circular dependencies exist using Kahn's algorithm.
105
+ * Build adjacency: planTitle -> depends on planTitles
106
+ * Run topological sort; if not all visited -> cycle exists.
107
+ */
108
+ validateNoCycles(specs: Array<{ title: string; dependencies?: PlanDependency[] }>): PlanValidationIssueInput[] {
109
+ const adj = new Map<string, Set<string>>()
110
+ const inDegree = new Map<string, number>()
111
+
112
+ for (const spec of specs) {
113
+ if (!adj.has(spec.title)) adj.set(spec.title, new Set())
114
+ if (!inDegree.has(spec.title)) inDegree.set(spec.title, 0)
115
+
116
+ for (const dep of spec.dependencies ?? []) {
117
+ if (!adj.has(dep.sourcePlanTitle)) adj.set(dep.sourcePlanTitle, new Set())
118
+ if (!inDegree.has(dep.sourcePlanTitle)) inDegree.set(dep.sourcePlanTitle, 0)
119
+
120
+ adj.get(dep.sourcePlanTitle)?.add(spec.title)
121
+ inDegree.set(spec.title, (inDegree.get(spec.title) ?? 0) + 1)
122
+ }
123
+ }
124
+
125
+ const queue = [...inDegree.entries()].filter(([, d]) => d === 0).map(([t]) => t)
126
+ const visited = new Set<string>()
127
+
128
+ while (queue.length > 0) {
129
+ const node = queue.shift()
130
+ if (!node) break
131
+ visited.add(node)
132
+ for (const dep of adj.get(node) ?? []) {
133
+ const d = (inDegree.get(dep) ?? 0) - 1
134
+ inDegree.set(dep, d)
135
+ if (d === 0) queue.push(dep)
136
+ }
137
+ }
138
+
139
+ const unvisited = specs.filter((s) => !visited.has(s.title))
140
+ if (unvisited.length === 0) return []
141
+
142
+ return [
143
+ {
144
+ severity: 'blocking',
145
+ code: 'circular_dependency',
146
+ message: `Circular plan dependencies detected involving: ${unvisited.map((s) => s.title).join(', ')}`,
147
+ },
148
+ ]
149
+ }
150
+ }
151
+
152
+ export const planCoordinationService = new PlanCoordinationService()