@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
@@ -3,6 +3,8 @@ import type {
3
3
  CreateExecutionPlanArgs,
4
4
  ExecutionPlanToolResultData,
5
5
  GetActiveExecutionPlanArgs,
6
+ ListExecutionPlansSummary,
7
+ ListExecutionPlansToolResultData,
6
8
  PlanNodeRunRecord,
7
9
  PlanNodeSpecRecord,
8
10
  PlanRunRecord,
@@ -22,6 +24,7 @@ import {
22
24
  } from '@lota-sdk/shared'
23
25
  import { RecordId } from 'surrealdb'
24
26
 
27
+ import { serverLogger } from '../config/logger'
25
28
  import type { RecordIdInput } from '../db/record-id'
26
29
  import { ensureRecordId, recordIdToString } from '../db/record-id'
27
30
  import { databaseService } from '../db/service'
@@ -29,13 +32,27 @@ import type { DatabaseTransaction } from '../db/service'
29
32
  import { TABLES } from '../db/tables'
30
33
  import { readApprovalContinuationResponse } from '../runtime/approval-continuation'
31
34
  import { extractMessageText } from '../runtime/workstream-chat-helpers'
35
+ import { toDatabaseDateTime } from '../utils/date-time'
36
+ import { contextEnrichmentService } from './context-enrichment.service'
37
+ import { ownershipDispatcherService } from './ownership-dispatcher.service'
32
38
  import { planBuilderService } from './plan-builder.service'
33
39
  import type { CompiledPlanNode } from './plan-compiler.service'
34
40
  import { planCompilerService } from './plan-compiler.service'
35
41
  import { planExecutorService } from './plan-executor.service'
36
42
  import { planRunService } from './plan-run.service'
43
+ import { planSchedulerService } from './plan-scheduler.service'
37
44
  import { planValidatorService } from './plan-validator.service'
38
45
 
46
+ export type ExecutionPlanDispatchMode = 'deferred' | 'stable-boundary'
47
+
48
+ const TOOL_RESULT_SERIALIZE_OPTIONS = {
49
+ includeEvents: true,
50
+ includeArtifacts: true,
51
+ includeApprovals: true,
52
+ includeCheckpoints: true,
53
+ includeValidationIssues: true,
54
+ } as const
55
+
39
56
  function buildToolResult(params: {
40
57
  action: ExecutionPlanToolResultData['action']
41
58
  plan: SerializableExecutionPlan | null
@@ -68,15 +85,23 @@ function toSpecData(spec: PlanSpecRecord, patch: Partial<PlanSpecRecord> & { rep
68
85
  schemaRegistry: patch.schemaRegistry ? structuredClone(patch.schemaRegistry) : structuredClone(spec.schemaRegistry),
69
86
  edges: patch.edges ? [...patch.edges] : [...spec.edges],
70
87
  entryNodeIds: patch.entryNodeIds ? [...patch.entryNodeIds] : [...spec.entryNodeIds],
88
+ executionMode: patch.executionMode ?? spec.executionMode,
89
+ ...(patch.schedule !== undefined ? { schedule: patch.schedule } : spec.schedule ? { schedule: spec.schedule } : {}),
90
+ ...(patch.dependencies !== undefined
91
+ ? { dependencies: patch.dependencies }
92
+ : spec.dependencies
93
+ ? { dependencies: spec.dependencies }
94
+ : {}),
95
+ ...(spec.contextEnrichments ? { contextEnrichments: spec.contextEnrichments } : {}),
71
96
  ...(patch.replacedSpecId
72
97
  ? { replacedSpecId: ensureRecordId(patch.replacedSpecId, TABLES.PLAN_SPEC) }
73
98
  : spec.replacedSpecId
74
99
  ? { replacedSpecId: ensureRecordId(spec.replacedSpecId, TABLES.PLAN_SPEC) }
75
100
  : {}),
76
101
  ...(patch.compiledAt !== undefined
77
- ? { compiledAt: patch.compiledAt }
102
+ ? { compiledAt: toDatabaseDateTime(patch.compiledAt) }
78
103
  : spec.compiledAt
79
- ? { compiledAt: spec.compiledAt }
104
+ ? { compiledAt: toDatabaseDateTime(spec.compiledAt) }
80
105
  : {}),
81
106
  }
82
107
  }
@@ -89,8 +114,8 @@ type PlanRunUpdate = Omit<
89
114
  waitingNodeId?: string | null
90
115
  replacedRunId?: RecordIdInput | null
91
116
  lastCheckpointId?: RecordIdInput | null
92
- startedAt?: Date | null
93
- completedAt?: Date | null
117
+ startedAt?: string | Date | null
118
+ completedAt?: string | Date | null
94
119
  }
95
120
 
96
121
  function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
@@ -133,16 +158,16 @@ function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
133
158
  ...(patch.startedAt === null
134
159
  ? {}
135
160
  : patch.startedAt !== undefined
136
- ? { startedAt: patch.startedAt }
161
+ ? { startedAt: toDatabaseDateTime(patch.startedAt) }
137
162
  : run.startedAt
138
- ? { startedAt: run.startedAt }
163
+ ? { startedAt: toDatabaseDateTime(run.startedAt) }
139
164
  : {}),
140
165
  ...(patch.completedAt === null
141
166
  ? {}
142
167
  : patch.completedAt !== undefined
143
- ? { completedAt: patch.completedAt }
168
+ ? { completedAt: toDatabaseDateTime(patch.completedAt) }
144
169
  : run.completedAt
145
- ? { completedAt: run.completedAt }
170
+ ? { completedAt: toDatabaseDateTime(run.completedAt) }
146
171
  : {}),
147
172
  }
148
173
  }
@@ -176,54 +201,142 @@ class ExecutionPlanService {
176
201
 
177
202
  async getActivePlanForWorkstream(
178
203
  workstreamId: RecordIdInput,
179
- options?: Partial<GetActiveExecutionPlanArgs>,
204
+ options?: Partial<GetActiveExecutionPlanArgs> & { runId?: RecordIdInput },
180
205
  ): Promise<SerializableExecutionPlan | null> {
181
- const run = await planRunService.getActiveRunRecord(workstreamId)
182
- if (!run) return null
206
+ const plans = await this.getActivePlansForWorkstream(workstreamId, options)
207
+ return plans[0] ?? null
208
+ }
183
209
 
184
- return await planRunService.toSerializablePlan(run, {
185
- includeEvents: options?.includeEvents,
186
- includeArtifacts: options?.includeArtifacts,
187
- includeApprovals: options?.includeApprovals,
188
- includeCheckpoints: options?.includeCheckpoints,
189
- includeValidationIssues: options?.includeValidationIssues,
190
- })
210
+ async getActivePlansForWorkstream(
211
+ workstreamId: RecordIdInput,
212
+ options?: Partial<GetActiveExecutionPlanArgs> & { runId?: RecordIdInput },
213
+ ): Promise<SerializableExecutionPlan[]> {
214
+ if (options?.runId) {
215
+ const run = await planRunService.getRunById(options.runId)
216
+ return [
217
+ await planRunService.toSerializablePlan(run, {
218
+ includeEvents: options.includeEvents,
219
+ includeArtifacts: options.includeArtifacts,
220
+ includeApprovals: options.includeApprovals,
221
+ includeCheckpoints: options.includeCheckpoints,
222
+ includeValidationIssues: options.includeValidationIssues,
223
+ }),
224
+ ]
225
+ }
226
+
227
+ const runs = await planRunService.getActiveRunRecords(workstreamId)
228
+ if (runs.length === 0) return []
229
+
230
+ return await Promise.all(
231
+ runs.map((run) =>
232
+ planRunService.toSerializablePlan(run, {
233
+ includeEvents: options?.includeEvents,
234
+ includeArtifacts: options?.includeArtifacts,
235
+ includeApprovals: options?.includeApprovals,
236
+ includeCheckpoints: options?.includeCheckpoints,
237
+ includeValidationIssues: options?.includeValidationIssues,
238
+ }),
239
+ ),
240
+ )
241
+ }
242
+
243
+ async listActivePlanSummaries(workstreamId: RecordIdInput): Promise<ListExecutionPlansToolResultData> {
244
+ const runs = await planRunService.getActiveRunRecords(workstreamId)
245
+ const plans: ListExecutionPlansSummary[] = await Promise.all(
246
+ runs.map(async (run) => {
247
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
248
+ const nodeRuns = await planRunService.listNodeRuns(run.id)
249
+ return {
250
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
251
+ title: spec.title,
252
+ status: run.status,
253
+ objective: spec.objective,
254
+ nodeCount: nodeRuns.length,
255
+ completedCount: nodeRuns.filter((n) => n.status === 'completed' || n.status === 'partial').length,
256
+ failedCount: nodeRuns.filter((n) => n.status === 'failed').length,
257
+ }
258
+ }),
259
+ )
260
+ return { plans, totalCount: plans.length }
191
261
  }
192
262
 
193
263
  async getActivePlanToolResult(params: {
194
264
  workstreamId: RecordIdInput
265
+ runId?: string
195
266
  includeEvents?: boolean
196
267
  includeArtifacts?: boolean
197
268
  includeApprovals?: boolean
198
269
  includeCheckpoints?: boolean
199
270
  includeValidationIssues?: boolean
200
271
  }): Promise<ExecutionPlanToolResultData> {
201
- const plan = await this.getActivePlanForWorkstream(params.workstreamId, params)
202
- return buildToolResult({
203
- action: plan ? 'loaded' : 'none',
204
- plan,
205
- message: plan ? `Loaded execution run "${plan.title}".` : 'No active execution run.',
206
- })
272
+ const serializeOptions = {
273
+ includeEvents: params.includeEvents,
274
+ includeArtifacts: params.includeArtifacts,
275
+ includeApprovals: params.includeApprovals,
276
+ includeCheckpoints: params.includeCheckpoints,
277
+ includeValidationIssues: params.includeValidationIssues,
278
+ }
279
+
280
+ if (params.runId) {
281
+ const plan = await planRunService
282
+ .getRunById(params.runId)
283
+ .then((run) => planRunService.toSerializablePlan(run, serializeOptions))
284
+ return buildToolResult({ action: 'loaded', plan, message: `Loaded execution run "${plan.title}".` })
285
+ }
286
+
287
+ const runs = await planRunService.getActiveRunRecords(params.workstreamId)
288
+ if (runs.length === 0) {
289
+ return buildToolResult({ action: 'none', plan: null, message: 'No active execution run.' })
290
+ }
291
+
292
+ const plan = await planRunService.toSerializablePlan(runs[0], serializeOptions)
293
+ const planSummaries = await Promise.all(
294
+ runs.map(async (run) => {
295
+ const spec = await planRunService.getPlanSpecById(run.planSpecId)
296
+ return { runId: recordIdToString(run.id, TABLES.PLAN_RUN), title: spec.title, status: run.status }
297
+ }),
298
+ )
299
+
300
+ return {
301
+ ...buildToolResult({
302
+ action: 'loaded',
303
+ plan,
304
+ message:
305
+ runs.length === 1
306
+ ? `Loaded execution run "${plan.title}".`
307
+ : `Loaded ${runs.length} active execution runs. Showing most recent: "${plan.title}".`,
308
+ }),
309
+ planCount: runs.length,
310
+ planSummaries,
311
+ }
207
312
  }
208
313
 
209
314
  async createPlan(params: {
210
315
  organizationId: RecordIdInput
211
316
  workstreamId: RecordIdInput
212
317
  leadAgentId: string
318
+ dispatchMode: ExecutionPlanDispatchMode
213
319
  input: CreateExecutionPlanArgs
214
320
  }): Promise<ExecutionPlanToolResultData> {
215
- const activeRun = await planRunService.getActiveRunRecord(params.workstreamId)
216
- if (activeRun) {
217
- throw new Error('An active execution run already exists for this workstream. Replace it instead.')
218
- }
219
-
220
321
  const preparedDraft = planBuilderService.prepareDraft(params.input)
221
322
  const validation = planValidatorService.validateDraft(preparedDraft)
222
323
  if (validation.blocking.length > 0) {
223
324
  throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`)
224
325
  }
326
+ await this.assertDispatchExecutors(preparedDraft)
225
327
  const compiled = planCompilerService.compile(preparedDraft)
226
328
 
329
+ // Context enrichment — best-effort, failures do not block plan creation
330
+ const enrichments = await contextEnrichmentService
331
+ .enrichForPlanCreation({
332
+ objective: compiled.draft.objective,
333
+ organizationId: recordIdToString(params.organizationId, TABLES.ORGANIZATION),
334
+ })
335
+ .catch((error: unknown) => {
336
+ serverLogger.error`Context enrichment failed: ${error instanceof Error ? error.message : String(error)}`
337
+ return []
338
+ })
339
+
227
340
  const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
228
341
  const runId = new RecordId(TABLES.PLAN_RUN, Bun.randomUUIDv7())
229
342
 
@@ -240,8 +353,14 @@ class ExecutionPlanService {
240
353
  status: 'compiled',
241
354
  leadAgentId: params.leadAgentId,
242
355
  schemaRegistry: structuredClone(compiled.draft.schemas),
356
+ ...(enrichments.length > 0
357
+ ? { contextEnrichments: enrichments.map((e) => ({ type: e.domain, content: JSON.stringify(e.data) })) }
358
+ : {}),
243
359
  edges: [...compiled.draft.edges],
244
360
  entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
361
+ executionMode: compiled.draft.executionMode ?? 'linear',
362
+ ...(compiled.draft.schedule ? { schedule: compiled.draft.schedule } : {}),
363
+ ...(compiled.draft.dependencies ? { dependencies: compiled.draft.dependencies } : {}),
245
364
  compiledAt: new Date(),
246
365
  })
247
366
  .output('after'),
@@ -332,12 +451,31 @@ class ExecutionPlanService {
332
451
  PlanEventSchema.parse(checkpointEvent)
333
452
  })
334
453
 
335
- const plan = await planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
336
- includeEvents: true,
337
- includeArtifacts: true,
338
- includeApprovals: true,
339
- includeCheckpoints: true,
340
- includeValidationIssues: true,
454
+ // Create a plan-level schedule record if the draft specifies a schedule
455
+ if (compiled.draft.schedule) {
456
+ const schedule = await planSchedulerService.createSchedule({
457
+ organizationId: params.organizationId,
458
+ workstreamId: params.workstreamId,
459
+ planSpecId: specId,
460
+ runId,
461
+ scheduleSpec: compiled.draft.schedule,
462
+ })
463
+
464
+ await databaseService.update(
465
+ TABLES.PLAN_RUN,
466
+ ensureRecordId(runId, TABLES.PLAN_RUN),
467
+ {
468
+ scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE),
469
+ scheduledAt: schedule.nextFireAt ? toDatabaseDateTime(schedule.nextFireAt) : undefined,
470
+ },
471
+ PlanRunSchema,
472
+ )
473
+ }
474
+
475
+ const plan = await this.finalizePlanSnapshot({
476
+ runId,
477
+ emittedBy: params.leadAgentId,
478
+ dispatchMode: params.dispatchMode,
341
479
  })
342
480
 
343
481
  return buildToolResult({ action: 'created', plan, message: `Created execution plan "${plan.title}".` })
@@ -347,14 +485,23 @@ class ExecutionPlanService {
347
485
  workstreamId: RecordIdInput
348
486
  organizationId: RecordIdInput
349
487
  leadAgentId: string
488
+ dispatchMode: ExecutionPlanDispatchMode
350
489
  input: ReplaceExecutionPlanArgs
351
490
  }): Promise<ExecutionPlanToolResultData> {
352
- const activeRun = await planRunService.getActiveRunRecord(params.workstreamId)
353
- if (!activeRun) {
491
+ const activeRun = await planRunService.getRunById(params.input.runId)
492
+ if (
493
+ recordIdToString(activeRun.workstreamId, TABLES.WORKSTREAM) !==
494
+ recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
495
+ ) {
496
+ throw new Error('Execution run belongs to a different workstream.')
497
+ }
498
+
499
+ const activeRuns = await planRunService.getActiveRunRecords(params.workstreamId)
500
+ if (activeRuns.length === 0) {
354
501
  throw new Error('No active execution run exists for this workstream.')
355
502
  }
356
- if (recordIdToString(activeRun.id, TABLES.PLAN_RUN) !== params.input.runId) {
357
- throw new Error('Only the active execution run can be replaced.')
503
+ if (!activeRuns.some((run) => recordIdToString(run.id, TABLES.PLAN_RUN) === params.input.runId)) {
504
+ throw new Error('Only an active execution run can be replaced.')
358
505
  }
359
506
 
360
507
  const activeSpec = await planRunService.getPlanSpecById(activeRun.planSpecId)
@@ -365,11 +512,15 @@ class ExecutionPlanService {
365
512
  edges: params.input.edges,
366
513
  entryNodeIds: params.input.entryNodeIds,
367
514
  schemas: params.input.schemas,
515
+ executionMode: params.input.executionMode,
516
+ schedule: params.input.schedule,
517
+ dependencies: params.input.dependencies,
368
518
  })
369
519
  const validation = planValidatorService.validateDraft(preparedDraft)
370
520
  if (validation.blocking.length > 0) {
371
521
  throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(validation.blocking)}`)
372
522
  }
523
+ await this.assertDispatchExecutors(preparedDraft)
373
524
  const compiled = planCompilerService.compile(preparedDraft)
374
525
 
375
526
  const specId = new RecordId(TABLES.PLAN_SPEC, Bun.randomUUIDv7())
@@ -412,6 +563,9 @@ class ExecutionPlanService {
412
563
  schemaRegistry: structuredClone(compiled.draft.schemas),
413
564
  edges: [...compiled.draft.edges],
414
565
  entryNodeIds: [...(compiled.draft.entryNodeIds ?? [])],
566
+ executionMode: compiled.draft.executionMode ?? 'linear',
567
+ ...(compiled.draft.schedule ? { schedule: compiled.draft.schedule } : {}),
568
+ ...(compiled.draft.dependencies ? { dependencies: compiled.draft.dependencies } : {}),
415
569
  replacedSpecId: ensureRecordId(supersededSpec.id, TABLES.PLAN_SPEC),
416
570
  compiledAt: new Date(),
417
571
  })
@@ -504,12 +658,10 @@ class ExecutionPlanService {
504
658
  PlanEventSchema.parse(checkpointEvent)
505
659
  })
506
660
 
507
- const plan = await planRunService.toSerializablePlan(await planRunService.getRunById(runId), {
508
- includeEvents: true,
509
- includeArtifacts: true,
510
- includeApprovals: true,
511
- includeCheckpoints: true,
512
- includeValidationIssues: true,
661
+ const plan = await this.finalizePlanSnapshot({
662
+ runId,
663
+ emittedBy: params.leadAgentId,
664
+ dispatchMode: params.dispatchMode,
513
665
  })
514
666
 
515
667
  return buildToolResult({ action: 'replaced', plan, message: `Replaced execution plan with "${plan.title}".` })
@@ -520,13 +672,24 @@ class ExecutionPlanService {
520
672
  emittedBy: string
521
673
  input: SubmitExecutionNodeResultArgs
522
674
  }): Promise<ExecutionPlanToolResultData> {
523
- return await planExecutorService.submitNodeResult({
675
+ const result = await planExecutorService.submitNodeResult({
524
676
  workstreamId: params.workstreamId,
525
677
  runId: params.input.runId,
526
678
  nodeId: params.input.nodeId,
527
679
  emittedBy: params.emittedBy,
528
680
  result: params.input.result,
529
681
  })
682
+ const plan = await ownershipDispatcherService.dispatchRunToStableBoundary({
683
+ runId: params.input.runId,
684
+ emittedBy: params.emittedBy,
685
+ })
686
+
687
+ return buildToolResult({
688
+ action: result.action,
689
+ plan,
690
+ message: result.message ?? `Submitted result for node "${params.input.nodeId}".`,
691
+ changedNodeId: result.changedNodeId ?? undefined,
692
+ })
530
693
  }
531
694
 
532
695
  async resumeRun(params: {
@@ -534,11 +697,22 @@ class ExecutionPlanService {
534
697
  emittedBy: string
535
698
  input: ResumeExecutionPlanRunArgs
536
699
  }): Promise<ExecutionPlanToolResultData> {
537
- return await planExecutorService.resumeRun({
700
+ const result = await planExecutorService.resumeRun({
538
701
  workstreamId: params.workstreamId,
539
702
  runId: params.input.runId,
540
703
  emittedBy: params.emittedBy,
541
704
  })
705
+ const plan = await ownershipDispatcherService.dispatchRunToStableBoundary({
706
+ runId: params.input.runId,
707
+ emittedBy: params.emittedBy,
708
+ })
709
+
710
+ return buildToolResult({
711
+ action: result.action,
712
+ plan,
713
+ message: result.message ?? `Resumed execution run "${params.input.runId}".`,
714
+ changedNodeId: result.changedNodeId ?? undefined,
715
+ })
542
716
  }
543
717
 
544
718
  async applyApprovalResponseFromMessages(params: {
@@ -549,12 +723,18 @@ class ExecutionPlanService {
549
723
  const approvalResponse = buildApprovalResponseFromMessages(params.approvalMessages)
550
724
  if (!approvalResponse) return null
551
725
 
552
- return await planExecutorService.submitHumanNodeResponse({
726
+ const run = await planRunService.getActiveRunRecord(params.workstreamId)
727
+ if (!run) return null
728
+
729
+ const plan = await planExecutorService.submitHumanNodeResponse({
553
730
  workstreamId: params.workstreamId,
554
731
  approvalId: approvalResponse.approvalId,
555
732
  respondedBy: params.respondedBy,
556
733
  response: approvalResponse.response,
557
734
  })
735
+ if (!plan) return null
736
+
737
+ return ownershipDispatcherService.dispatchRunToStableBoundary({ runId: run.id, emittedBy: params.respondedBy })
558
738
  }
559
739
 
560
740
  async respondToApproval(params: {
@@ -562,13 +742,19 @@ class ExecutionPlanService {
562
742
  emittedBy: string
563
743
  input: { approvalId: string; response: Record<string, unknown>; approvalMessageId?: string }
564
744
  }): Promise<SerializableExecutionPlan | null> {
565
- return await planExecutorService.submitHumanNodeResponse({
745
+ const run = await planRunService.getActiveRunRecord(params.workstreamId)
746
+ if (!run) return null
747
+
748
+ const plan = await planExecutorService.submitHumanNodeResponse({
566
749
  workstreamId: params.workstreamId,
567
750
  approvalId: params.input.approvalId,
568
751
  respondedBy: params.emittedBy,
569
752
  response: params.input.response,
570
753
  approvalMessageId: params.input.approvalMessageId,
571
754
  })
755
+ if (!plan) return null
756
+
757
+ return ownershipDispatcherService.dispatchRunToStableBoundary({ runId: run.id, emittedBy: params.emittedBy })
572
758
  }
573
759
 
574
760
  async applyHumanInputFromUserMessage(params: {
@@ -597,12 +783,38 @@ class ExecutionPlanService {
597
783
  approved: nodeSpec.type === 'human-decision' ? true : undefined,
598
784
  } satisfies Record<string, unknown>
599
785
 
600
- return await planExecutorService.submitHumanNodeResponse({
786
+ const plan = await planExecutorService.submitHumanNodeResponse({
601
787
  workstreamId: params.workstreamId,
602
788
  respondedBy: params.respondedBy,
603
789
  response,
604
790
  approvalMessageId: params.message.id,
605
791
  })
792
+ if (!plan) return null
793
+
794
+ return ownershipDispatcherService.dispatchRunToStableBoundary({ runId: run.id, emittedBy: params.respondedBy })
795
+ }
796
+
797
+ private async assertDispatchExecutors(preparedDraft: Parameters<typeof planValidatorService.validateDraft>[0]) {
798
+ const issues = ownershipDispatcherService.validateDraftExecutors(preparedDraft)
799
+ if (issues.length > 0) {
800
+ throw new Error(`Plan draft failed validation: ${aggregateBlockingIssues(issues)}`)
801
+ }
802
+ }
803
+
804
+ private async finalizePlanSnapshot(params: {
805
+ runId: RecordIdInput
806
+ emittedBy: string
807
+ dispatchMode: ExecutionPlanDispatchMode
808
+ }): Promise<SerializableExecutionPlan> {
809
+ if (params.dispatchMode === 'stable-boundary') {
810
+ return ownershipDispatcherService.dispatchRunToStableBoundary({
811
+ runId: params.runId,
812
+ emittedBy: params.emittedBy,
813
+ })
814
+ }
815
+
816
+ const run = await planRunService.getRunById(params.runId)
817
+ return planRunService.toSerializablePlan(run, TOOL_RESULT_SERIALIZE_OPTIONS)
606
818
  }
607
819
 
608
820
  private async createNodeSpecs(
@@ -612,6 +824,7 @@ class ExecutionPlanService {
612
824
  ): Promise<PlanNodeSpecRecord[]> {
613
825
  const createdRecords: PlanNodeSpecRecord[] = []
614
826
 
827
+ // Sequential: SurrealDB transactions require ordered operations
615
828
  for (const compiledNode of nodes) {
616
829
  const nodeSpecId = new RecordId(TABLES.PLAN_NODE_SPEC, Bun.randomUUIDv7())
617
830
  const created = await tx
@@ -639,6 +852,13 @@ class ExecutionPlanService {
639
852
  attachmentPolicy: compiledNode.node.contextPolicy.attachmentPolicy,
640
853
  webPolicy: compiledNode.node.contextPolicy.webPolicy,
641
854
  },
855
+ ...(compiledNode.node.schedule ? { schedule: compiledNode.node.schedule } : {}),
856
+ ...(compiledNode.node.deadline ? { deadline: compiledNode.node.deadline } : {}),
857
+ ...(compiledNode.node.monitoringConfig ? { monitoringConfig: compiledNode.node.monitoringConfig } : {}),
858
+ ...(compiledNode.node.delayAfterPredecessorMs
859
+ ? { delayAfterPredecessorMs: compiledNode.node.delayAfterPredecessorMs }
860
+ : {}),
861
+ ...(compiledNode.node.deliberationConfig ? { deliberationConfig: compiledNode.node.deliberationConfig } : {}),
642
862
  upstreamNodeIds: [...compiledNode.upstreamNodeIds],
643
863
  downstreamNodeIds: [...compiledNode.downstreamNodeIds],
644
864
  })
@@ -657,6 +877,7 @@ class ExecutionPlanService {
657
877
  nodeSpecs: PlanNodeSpecRecord[],
658
878
  ): Promise<PlanNodeRunRecord[]> {
659
879
  const createdNodeRuns: PlanNodeRunRecord[] = []
880
+ // Sequential: SurrealDB transactions require ordered operations
660
881
  for (const nodeSpec of nodeSpecs) {
661
882
  const nodeRunId = new RecordId(TABLES.PLAN_NODE_RUN, Bun.randomUUIDv7())
662
883
  const created = await tx
@@ -0,0 +1,96 @@
1
+ import type { Recommendation } from '@lota-sdk/shared'
2
+
3
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
4
+ import { TABLES } from '../db/tables'
5
+ import { toIsoDateTimeString } from '../utils/date-time'
6
+ import { planRunService } from './plan-run.service'
7
+
8
+ class FeedbackLoopService {
9
+ async analyzeOutcomes(params: { runId: string; organizationId: string }): Promise<Recommendation[]> {
10
+ const run = await planRunService.getRunById(params.runId)
11
+ const nodeRuns = await planRunService.listNodeRuns(run.id)
12
+ const attempts = await planRunService.listAttempts(run.id)
13
+ const nodeSpecs = await planRunService.listNodeSpecs(ensureRecordId(run.planSpecId, TABLES.PLAN_SPEC))
14
+ const nodeSpecsByNodeId = new Map(nodeSpecs.map((ns) => [ns.nodeId, ns]))
15
+
16
+ const recommendations: Recommendation[] = []
17
+
18
+ for (const nodeRun of nodeRuns) {
19
+ const nodeSpec = nodeSpecsByNodeId.get(nodeRun.nodeId)
20
+ if (!nodeSpec) continue
21
+
22
+ const nodeAttempts = attempts.filter((a) => a.nodeId === nodeRun.nodeId)
23
+ const failedAttempts = nodeAttempts.filter((a) => a.status === 'failed')
24
+
25
+ if (failedAttempts.length >= 2) {
26
+ recommendations.push({
27
+ type: 'warning',
28
+ target: 'node',
29
+ targetId: nodeRun.nodeId,
30
+ description: `Node "${nodeSpec.label}" failed ${failedAttempts.length} times before ${nodeRun.status === 'completed' || nodeRun.status === 'partial' ? 'succeeding' : 'giving up'}. Consider adjusting retry policy or node instructions.`,
31
+ evidence: failedAttempts.map((a) => ({
32
+ sourceType: 'metric',
33
+ sourceId: recordIdToString(a.id, TABLES.PLAN_NODE_ATTEMPT),
34
+ summary: `Attempt failed with class: ${a.failureClass ?? 'unknown'}`,
35
+ confidence: 0.9,
36
+ })),
37
+ confidence: Math.min(0.5 + failedAttempts.length * 0.15, 0.95),
38
+ })
39
+ }
40
+ }
41
+
42
+ const completedNodeRuns = nodeRuns.filter(
43
+ (nr) => nr.startedAt && nr.completedAt && (nr.status === 'completed' || nr.status === 'partial'),
44
+ )
45
+ if (completedNodeRuns.length >= 2) {
46
+ const durations = completedNodeRuns.map((nr) => {
47
+ const start = new Date(toIsoDateTimeString(nr.startedAt)).getTime()
48
+ const end = new Date(toIsoDateTimeString(nr.completedAt)).getTime()
49
+ return { nodeId: nr.nodeId, durationMs: end - start }
50
+ })
51
+
52
+ const avgDuration = durations.reduce((sum, d) => sum + d.durationMs, 0) / durations.length
53
+
54
+ for (const d of durations) {
55
+ if (d.durationMs > avgDuration * 2.5 && d.durationMs > 5000) {
56
+ const nodeSpec = nodeSpecsByNodeId.get(d.nodeId)
57
+ recommendations.push({
58
+ type: 'optimization',
59
+ target: 'node',
60
+ targetId: d.nodeId,
61
+ description: `Node "${nodeSpec?.label ?? d.nodeId}" took ${Math.round(d.durationMs / 1000)}s, which is ${Math.round((d.durationMs / avgDuration) * 10) / 10}x the average. Consider splitting or optimizing.`,
62
+ evidence: [
63
+ {
64
+ sourceType: 'metric',
65
+ sourceId: d.nodeId,
66
+ summary: `Execution time: ${d.durationMs}ms vs average ${Math.round(avgDuration)}ms`,
67
+ confidence: 0.85,
68
+ },
69
+ ],
70
+ confidence: 0.7,
71
+ })
72
+ }
73
+ }
74
+ }
75
+
76
+ const skippedNodes = nodeRuns.filter((nr) => nr.status === 'skipped')
77
+ if (skippedNodes.length > 0 && skippedNodes.length >= nodeRuns.length * 0.3) {
78
+ recommendations.push({
79
+ type: 'pattern',
80
+ target: 'plan',
81
+ description: `${skippedNodes.length} of ${nodeRuns.length} nodes were skipped. The plan may have overly broad conditional branches.`,
82
+ evidence: skippedNodes.map((nr) => ({
83
+ sourceType: 'pattern',
84
+ sourceId: nr.nodeId,
85
+ summary: `Node "${nodeSpecsByNodeId.get(nr.nodeId)?.label ?? nr.nodeId}" was skipped`,
86
+ confidence: 0.8,
87
+ })),
88
+ confidence: 0.65,
89
+ })
90
+ }
91
+
92
+ return recommendations
93
+ }
94
+ }
95
+
96
+ export const feedbackLoopService = new FeedbackLoopService()