@lota-sdk/core 0.1.9 → 0.1.12

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 (105) hide show
  1. package/infrastructure/schema/00_workstream.surql +1 -0
  2. package/infrastructure/schema/02_execution_plan.surql +202 -52
  3. package/package.json +4 -87
  4. package/src/ai/index.ts +3 -0
  5. package/src/bifrost/bifrost.ts +94 -25
  6. package/src/bifrost/index.ts +1 -0
  7. package/src/config/agent-defaults.ts +30 -7
  8. package/src/config/constants.ts +0 -9
  9. package/src/config/debug-logger.ts +43 -0
  10. package/src/config/index.ts +5 -0
  11. package/src/config/model-constants.ts +8 -9
  12. package/src/config/workstream-defaults.ts +4 -0
  13. package/src/db/cursor-pagination.ts +2 -2
  14. package/src/db/index.ts +10 -0
  15. package/src/db/memory-store.ts +3 -71
  16. package/src/db/memory.ts +9 -15
  17. package/src/db/service.ts +42 -2
  18. package/src/db/tables.ts +9 -2
  19. package/src/document/index.ts +2 -0
  20. package/src/document/parsing.ts +0 -25
  21. package/src/embeddings/provider.ts +102 -22
  22. package/src/index.ts +15 -499
  23. package/src/queues/index.ts +10 -0
  24. package/src/redis/connection-accessor.ts +26 -0
  25. package/src/redis/connection.ts +1 -1
  26. package/src/redis/index.ts +9 -25
  27. package/src/redis/org-memory-lock.ts +1 -1
  28. package/src/redis/redis-lease-lock.ts +1 -1
  29. package/src/redis/stream-context.ts +54 -0
  30. package/src/runtime/agent-runtime-policy.ts +9 -5
  31. package/src/runtime/agent-stream-helpers.ts +6 -3
  32. package/src/runtime/agent-types.ts +1 -5
  33. package/src/runtime/approval-continuation.ts +68 -1
  34. package/src/runtime/chat-attachments.ts +1 -1
  35. package/src/runtime/chat-request-routing.ts +6 -2
  36. package/src/runtime/context-compaction-runtime.ts +2 -2
  37. package/src/runtime/context-compaction.ts +1 -1
  38. package/src/runtime/execution-plan.ts +22 -15
  39. package/src/runtime/index.ts +26 -0
  40. package/src/runtime/indexed-repositories-policy.ts +10 -10
  41. package/src/runtime/memory-pipeline.ts +0 -2
  42. package/src/runtime/runtime-config.ts +238 -0
  43. package/src/runtime/runtime-extensions.ts +3 -2
  44. package/src/runtime/runtime-worker-registry.ts +47 -0
  45. package/src/runtime/team-consultation-orchestrator.ts +9 -6
  46. package/src/runtime/team-consultation-prompts.ts +3 -2
  47. package/src/runtime/turn-lifecycle.ts +13 -5
  48. package/src/runtime/workstream-chat-helpers.ts +0 -54
  49. package/src/runtime/workstream-routing-policy.ts +3 -7
  50. package/src/runtime.ts +387 -0
  51. package/src/services/chat-attachments.service.ts +1 -1
  52. package/src/services/context-compaction.service.ts +1 -1
  53. package/src/services/document-chunk.service.ts +2 -2
  54. package/src/services/execution-plan.service.ts +584 -793
  55. package/src/services/index.ts +14 -0
  56. package/src/services/learned-skill.service.ts +82 -39
  57. package/src/services/memory.service.ts +5 -4
  58. package/src/services/mutating-approval.service.ts +1 -1
  59. package/src/services/organization-member.service.ts +1 -1
  60. package/src/services/organization.service.ts +1 -1
  61. package/src/services/plan-approval.service.ts +83 -0
  62. package/src/services/plan-artifact.service.ts +44 -0
  63. package/src/services/plan-builder.service.ts +61 -0
  64. package/src/services/plan-checkpoint.service.ts +53 -0
  65. package/src/services/plan-compiler.service.ts +81 -0
  66. package/src/services/plan-executor.service.ts +1624 -0
  67. package/src/services/plan-run.service.ts +422 -0
  68. package/src/services/plan-validator.service.ts +760 -0
  69. package/src/services/recent-activity-title.service.ts +1 -1
  70. package/src/services/recent-activity.service.ts +14 -16
  71. package/src/services/user.service.ts +2 -2
  72. package/src/services/workstream-message.service.ts +2 -3
  73. package/src/services/workstream-title.service.ts +1 -1
  74. package/src/services/workstream-turn-preparation.ts +156 -59
  75. package/src/services/workstream-turn.ts +26 -1
  76. package/src/services/workstream.service.ts +35 -9
  77. package/src/services/workstream.types.ts +1 -0
  78. package/src/storage/attachment-parser.ts +1 -1
  79. package/src/storage/attachment-storage.service.ts +11 -10
  80. package/src/storage/generated-document-storage.service.ts +7 -6
  81. package/src/storage/index.ts +10 -0
  82. package/src/system-agents/delegated-agent-factory.ts +78 -29
  83. package/src/system-agents/index.ts +4 -0
  84. package/src/system-agents/recent-activity-title-refiner.agent.ts +38 -3
  85. package/src/system-agents/regular-chat-memory-digest.agent.ts +1 -1
  86. package/src/system-agents/skill-extractor.agent.ts +1 -1
  87. package/src/system-agents/skill-manager.agent.ts +2 -4
  88. package/src/system-agents/title-generator.agent.ts +2 -2
  89. package/src/tools/execution-plan.tool.ts +22 -48
  90. package/src/tools/firecrawl-client.ts +2 -2
  91. package/src/tools/index.ts +12 -0
  92. package/src/tools/log-hello-world.tool.ts +17 -0
  93. package/src/tools/research-topic.tool.ts +1 -1
  94. package/src/tools/team-think.tool.ts +1 -1
  95. package/src/tools/user-questions.tool.ts +2 -2
  96. package/src/utils/index.ts +6 -0
  97. package/src/workers/bootstrap.ts +8 -16
  98. package/src/workers/index.ts +7 -0
  99. package/src/workers/regular-chat-memory-digest.runner.ts +1 -1
  100. package/src/workers/skill-extraction.runner.ts +3 -3
  101. package/src/workers/utils/{repo-indexer-chunker.ts → file-section-chunker.ts} +23 -52
  102. package/src/workers/utils/repo-structure-extractor.ts +2 -5
  103. package/src/workers/utils/repomix-file-sections.ts +42 -0
  104. package/src/config/env-shapes.ts +0 -121
  105. package/src/runtime/agent-contract.ts +0 -1
@@ -0,0 +1,422 @@
1
+ import {
2
+ PlanApprovalSchema,
3
+ PlanArtifactSchema,
4
+ PlanCheckpointSchema,
5
+ PlanEventSchema,
6
+ PlanNodeAttemptSchema,
7
+ PlanNodeRunSchema,
8
+ PlanNodeSpecRecordSchema,
9
+ PlanRunSchema,
10
+ PlanSpecSchema,
11
+ PlanValidationIssueSchema,
12
+ } from '@lota-sdk/shared'
13
+ import type {
14
+ PlanApprovalRecord,
15
+ PlanArtifactRecord,
16
+ PlanCheckpointRecord,
17
+ PlanEventRecord,
18
+ PlanNodeAttemptRecord,
19
+ PlanNodeRunRecord,
20
+ PlanNodeRunStatus,
21
+ PlanNodeSpecRecord,
22
+ PlanRunRecord,
23
+ PlanSpecRecord,
24
+ PlanValidationIssueRecord,
25
+ SerializableExecutionPlan,
26
+ SerializablePlanApproval,
27
+ SerializablePlanArtifact,
28
+ SerializablePlanCheckpoint,
29
+ SerializablePlanEvent,
30
+ SerializablePlanNode,
31
+ SerializablePlanValidationIssue,
32
+ } from '@lota-sdk/shared'
33
+
34
+ import type { RecordIdInput } from '../db/record-id'
35
+ import { ensureRecordId, recordIdToString } from '../db/record-id'
36
+ import { databaseService } from '../db/service'
37
+ import { TABLES } from '../db/tables'
38
+ import { toIsoDateTimeString, toOptionalIsoDateTimeString } from '../utils/date-time'
39
+
40
+ const ACTIVE_RUN_STATUSES = new Set(['running', 'awaiting-human', 'blocked'])
41
+
42
+ function buildProgress(nodeRuns: PlanNodeRunRecord[]) {
43
+ const counts = nodeRuns.reduce(
44
+ (summary, nodeRun) => {
45
+ summary[nodeRun.status] += 1
46
+ return summary
47
+ },
48
+ {
49
+ pending: 0,
50
+ ready: 0,
51
+ running: 0,
52
+ 'awaiting-human': 0,
53
+ completed: 0,
54
+ partial: 0,
55
+ blocked: 0,
56
+ failed: 0,
57
+ skipped: 0,
58
+ } satisfies Record<PlanNodeRunStatus, number>,
59
+ )
60
+
61
+ const total = nodeRuns.length
62
+ const completedWork = counts.completed + counts.partial + counts.skipped
63
+
64
+ return {
65
+ total,
66
+ pending: counts.pending,
67
+ ready: counts.ready,
68
+ running: counts.running,
69
+ awaitingHuman: counts['awaiting-human'],
70
+ completed: counts.completed,
71
+ partial: counts.partial,
72
+ blocked: counts.blocked,
73
+ failed: counts.failed,
74
+ skipped: counts.skipped,
75
+ completionRatio: total > 0 ? Number((completedWork / total).toFixed(4)) : undefined,
76
+ }
77
+ }
78
+
79
+ function serializeArtifact(artifact: PlanArtifactRecord): SerializablePlanArtifact {
80
+ return {
81
+ id: recordIdToString(artifact.id, TABLES.PLAN_ARTIFACT),
82
+ nodeId: artifact.nodeId,
83
+ attemptId: recordIdToString(artifact.attemptId, TABLES.PLAN_NODE_ATTEMPT),
84
+ name: artifact.name,
85
+ kind: artifact.kind,
86
+ pointer: artifact.pointer,
87
+ schemaRef: artifact.schemaRef,
88
+ description: artifact.description,
89
+ payload: artifact.payload,
90
+ createdAt: toIsoDateTimeString(artifact.createdAt),
91
+ }
92
+ }
93
+
94
+ function serializeValidationIssue(issue: PlanValidationIssueRecord): SerializablePlanValidationIssue {
95
+ return {
96
+ id: recordIdToString(issue.id, TABLES.PLAN_VALIDATION_ISSUE),
97
+ nodeId: issue.nodeId,
98
+ attemptId: issue.attemptId ? recordIdToString(issue.attemptId, TABLES.PLAN_NODE_ATTEMPT) : undefined,
99
+ severity: issue.severity,
100
+ code: issue.code,
101
+ message: issue.message,
102
+ detail: issue.detail,
103
+ createdAt: toIsoDateTimeString(issue.createdAt),
104
+ }
105
+ }
106
+
107
+ function serializeApproval(approval: PlanApprovalRecord): SerializablePlanApproval {
108
+ return {
109
+ id: recordIdToString(approval.id, TABLES.PLAN_APPROVAL),
110
+ nodeId: approval.nodeId,
111
+ status: approval.status,
112
+ presented: approval.presented,
113
+ response: approval.response,
114
+ requestedBy: approval.requestedBy,
115
+ respondedBy: approval.respondedBy,
116
+ approvalMessageId: approval.approvalMessageId,
117
+ comments: approval.comments,
118
+ requiredEdits: [...approval.requiredEdits],
119
+ createdAt: toIsoDateTimeString(approval.createdAt),
120
+ respondedAt: toOptionalIsoDateTimeString(approval.respondedAt),
121
+ }
122
+ }
123
+
124
+ function serializeCheckpoint(checkpoint: PlanCheckpointRecord): SerializablePlanCheckpoint {
125
+ return {
126
+ id: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT),
127
+ sequence: checkpoint.sequence,
128
+ runStatus: checkpoint.runStatus,
129
+ readyNodeIds: [...checkpoint.readyNodeIds],
130
+ activeNodeIds: [...checkpoint.activeNodeIds],
131
+ artifactIds: checkpoint.artifactIds.map((artifactId) => recordIdToString(artifactId, TABLES.PLAN_ARTIFACT)),
132
+ lastCompletedNodeIds: [...checkpoint.lastCompletedNodeIds],
133
+ snapshot: checkpoint.snapshot,
134
+ createdAt: toIsoDateTimeString(checkpoint.createdAt),
135
+ }
136
+ }
137
+
138
+ function serializeEvent(event: PlanEventRecord): SerializablePlanEvent {
139
+ return {
140
+ id: recordIdToString(event.id, TABLES.PLAN_EVENT),
141
+ nodeId: event.nodeId,
142
+ attemptId: event.attemptId ? recordIdToString(event.attemptId, TABLES.PLAN_NODE_ATTEMPT) : undefined,
143
+ approvalId: event.approvalId ? recordIdToString(event.approvalId, TABLES.PLAN_APPROVAL) : undefined,
144
+ eventType: event.eventType,
145
+ fromStatus: event.fromStatus,
146
+ toStatus: event.toStatus,
147
+ message: event.message,
148
+ detail: event.detail,
149
+ emittedBy: event.emittedBy,
150
+ createdAt: toIsoDateTimeString(event.createdAt),
151
+ }
152
+ }
153
+
154
+ class PlanRunService {
155
+ async getPlanSpecById(planSpecId: RecordIdInput): Promise<PlanSpecRecord> {
156
+ const spec = await databaseService.findOne(
157
+ TABLES.PLAN_SPEC,
158
+ { id: ensureRecordId(planSpecId, TABLES.PLAN_SPEC) },
159
+ PlanSpecSchema,
160
+ )
161
+ if (!spec) {
162
+ throw new Error(`Plan spec not found: ${recordIdToString(planSpecId, TABLES.PLAN_SPEC)}`)
163
+ }
164
+ return spec
165
+ }
166
+
167
+ async listNodeSpecs(planSpecId: RecordIdInput): Promise<PlanNodeSpecRecord[]> {
168
+ return await databaseService.findMany(
169
+ TABLES.PLAN_NODE_SPEC,
170
+ { planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC) },
171
+ PlanNodeSpecRecordSchema,
172
+ { orderBy: 'position', orderDir: 'ASC' },
173
+ )
174
+ }
175
+
176
+ async getNodeSpecByNodeId(planSpecId: RecordIdInput, nodeId: string): Promise<PlanNodeSpecRecord> {
177
+ const nodeSpec = await databaseService.findOne(
178
+ TABLES.PLAN_NODE_SPEC,
179
+ { planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC), nodeId },
180
+ PlanNodeSpecRecordSchema,
181
+ )
182
+ if (!nodeSpec) {
183
+ throw new Error(`Plan node spec "${nodeId}" not found.`)
184
+ }
185
+ return nodeSpec
186
+ }
187
+
188
+ async getRunById(runId: RecordIdInput): Promise<PlanRunRecord> {
189
+ const run = await databaseService.findOne(
190
+ TABLES.PLAN_RUN,
191
+ { id: ensureRecordId(runId, TABLES.PLAN_RUN) },
192
+ PlanRunSchema,
193
+ )
194
+ if (!run) {
195
+ throw new Error(`Plan run not found: ${recordIdToString(runId, TABLES.PLAN_RUN)}`)
196
+ }
197
+ return run
198
+ }
199
+
200
+ async getActiveRunRecord(workstreamId: RecordIdInput): Promise<PlanRunRecord | null> {
201
+ const runs = await databaseService.findMany(
202
+ TABLES.PLAN_RUN,
203
+ { workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
204
+ PlanRunSchema,
205
+ { orderBy: 'updatedAt', orderDir: 'DESC' },
206
+ )
207
+
208
+ return runs.find((run) => ACTIVE_RUN_STATUSES.has(run.status)) ?? null
209
+ }
210
+
211
+ async listNodeRuns(runId: RecordIdInput): Promise<PlanNodeRunRecord[]> {
212
+ return await databaseService.findMany(
213
+ TABLES.PLAN_NODE_RUN,
214
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
215
+ PlanNodeRunSchema,
216
+ { orderBy: 'nodeId', orderDir: 'ASC' },
217
+ )
218
+ }
219
+
220
+ async getNodeRunByNodeId(runId: RecordIdInput, nodeId: string): Promise<PlanNodeRunRecord> {
221
+ const nodeRun = await databaseService.findOne(
222
+ TABLES.PLAN_NODE_RUN,
223
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN), nodeId },
224
+ PlanNodeRunSchema,
225
+ )
226
+ if (!nodeRun) {
227
+ throw new Error(`Plan node run "${nodeId}" not found.`)
228
+ }
229
+ return nodeRun
230
+ }
231
+
232
+ async listArtifacts(runId: RecordIdInput): Promise<PlanArtifactRecord[]> {
233
+ return await databaseService.findMany(
234
+ TABLES.PLAN_ARTIFACT,
235
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
236
+ PlanArtifactSchema,
237
+ { orderBy: 'createdAt', orderDir: 'ASC' },
238
+ )
239
+ }
240
+
241
+ async listAttempts(runId: RecordIdInput): Promise<PlanNodeAttemptRecord[]> {
242
+ return await databaseService.findMany(
243
+ TABLES.PLAN_NODE_ATTEMPT,
244
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
245
+ PlanNodeAttemptSchema,
246
+ { orderBy: 'createdAt', orderDir: 'ASC' },
247
+ )
248
+ }
249
+
250
+ async listValidationIssues(params: {
251
+ runId?: RecordIdInput
252
+ planSpecId?: RecordIdInput
253
+ attemptId?: RecordIdInput
254
+ }): Promise<PlanValidationIssueRecord[]> {
255
+ const filter: Record<string, unknown> = {}
256
+ if (params.runId) filter.runId = ensureRecordId(params.runId, TABLES.PLAN_RUN)
257
+ if (params.planSpecId) filter.planSpecId = ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC)
258
+ if (params.attemptId) filter.attemptId = ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT)
259
+
260
+ return await databaseService.findMany(TABLES.PLAN_VALIDATION_ISSUE, filter, PlanValidationIssueSchema, {
261
+ orderBy: 'createdAt',
262
+ orderDir: 'ASC',
263
+ })
264
+ }
265
+
266
+ async listApprovals(runId: RecordIdInput): Promise<PlanApprovalRecord[]> {
267
+ return await databaseService.findMany(
268
+ TABLES.PLAN_APPROVAL,
269
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
270
+ PlanApprovalSchema,
271
+ { orderBy: 'createdAt', orderDir: 'ASC' },
272
+ )
273
+ }
274
+
275
+ async getLatestCheckpoint(runId: RecordIdInput): Promise<PlanCheckpointRecord | null> {
276
+ const checkpoints = await databaseService.findMany(
277
+ TABLES.PLAN_CHECKPOINT,
278
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
279
+ PlanCheckpointSchema,
280
+ { orderBy: 'sequence', orderDir: 'DESC', limit: 1 },
281
+ )
282
+
283
+ return checkpoints.at(0) ?? null
284
+ }
285
+
286
+ async getNextCheckpointSequence(runId: RecordIdInput): Promise<number> {
287
+ const latestCheckpoint = await this.getLatestCheckpoint(runId)
288
+ return latestCheckpoint ? latestCheckpoint.sequence + 1 : 1
289
+ }
290
+
291
+ async listEvents(runId: RecordIdInput, limit = 20): Promise<PlanEventRecord[]> {
292
+ return (
293
+ await databaseService.findMany(
294
+ TABLES.PLAN_EVENT,
295
+ { runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
296
+ PlanEventSchema,
297
+ { orderBy: 'createdAt', orderDir: 'DESC', limit },
298
+ )
299
+ ).reverse()
300
+ }
301
+
302
+ async toSerializablePlan(
303
+ run: PlanRunRecord,
304
+ options?: {
305
+ includeArtifacts?: boolean
306
+ includeApprovals?: boolean
307
+ includeCheckpoints?: boolean
308
+ includeEvents?: boolean
309
+ includeValidationIssues?: boolean
310
+ },
311
+ ): Promise<SerializableExecutionPlan> {
312
+ const spec = await this.getPlanSpecById(run.planSpecId)
313
+ const nodeSpecs = await this.listNodeSpecs(spec.id)
314
+ const nodeRuns = await this.listNodeRuns(run.id)
315
+ const artifacts = options?.includeArtifacts === false ? [] : await this.listArtifacts(run.id)
316
+ const lineageArtifacts = options?.includeArtifacts === false ? [] : await this.collectLineageArtifacts(run)
317
+ const approvals = options?.includeApprovals === false ? [] : await this.listApprovals(run.id)
318
+ const validationIssues =
319
+ options?.includeValidationIssues === false
320
+ ? []
321
+ : await this.listValidationIssues({ runId: run.id, planSpecId: spec.id })
322
+ const latestCheckpoint = options?.includeCheckpoints ? await this.getLatestCheckpoint(run.id) : null
323
+ const recentEvents = options?.includeEvents === false ? [] : await this.listEvents(run.id, 20)
324
+ const nodeRunsById = new Map(nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
325
+
326
+ const nodes: SerializablePlanNode[] = nodeSpecs.map((nodeSpec) => {
327
+ const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
328
+ if (!nodeRun) {
329
+ throw new Error(
330
+ `Plan run ${recordIdToString(run.id, TABLES.PLAN_RUN)} is missing node run "${nodeSpec.nodeId}".`,
331
+ )
332
+ }
333
+
334
+ return {
335
+ id: nodeSpec.nodeId,
336
+ type: nodeSpec.type,
337
+ label: nodeSpec.label,
338
+ owner: nodeSpec.owner,
339
+ objective: nodeSpec.objective,
340
+ instructions: nodeSpec.instructions,
341
+ inputSchemaRef: nodeSpec.inputSchemaRef,
342
+ outputSchemaRef: nodeSpec.outputSchemaRef,
343
+ deliverables: [...nodeSpec.deliverables],
344
+ successCriteria: [...nodeSpec.successCriteria],
345
+ completionChecks: [...nodeSpec.completionChecks],
346
+ retryPolicy: { ...nodeSpec.retryPolicy, retryOn: [...nodeSpec.retryPolicy.retryOn] },
347
+ failurePolicy: [...nodeSpec.failurePolicy],
348
+ timeoutMs: nodeSpec.timeoutMs,
349
+ toolPolicy: { allow: [...nodeSpec.toolPolicy.allow], deny: [...nodeSpec.toolPolicy.deny] },
350
+ contextPolicy: {
351
+ retrievalScopes: [...nodeSpec.contextPolicy.retrievalScopes],
352
+ attachmentPolicy: nodeSpec.contextPolicy.attachmentPolicy,
353
+ webPolicy: nodeSpec.contextPolicy.webPolicy,
354
+ },
355
+ status: nodeRun.status,
356
+ attemptCount: nodeRun.attemptCount,
357
+ retryCount: nodeRun.retryCount,
358
+ resolvedInput: nodeRun.resolvedInput,
359
+ latestStructuredOutput: nodeRun.latestStructuredOutput,
360
+ latestNotes: nodeRun.latestNotes,
361
+ blockedReason: nodeRun.blockedReason,
362
+ failureClass: nodeRun.failureClass,
363
+ upstreamNodeIds: [...nodeSpec.upstreamNodeIds],
364
+ downstreamNodeIds: [...nodeSpec.downstreamNodeIds],
365
+ readyAt: toOptionalIsoDateTimeString(nodeRun.readyAt),
366
+ startedAt: toOptionalIsoDateTimeString(nodeRun.startedAt),
367
+ completedAt: toOptionalIsoDateTimeString(nodeRun.completedAt),
368
+ }
369
+ })
370
+
371
+ return {
372
+ specId: recordIdToString(spec.id, TABLES.PLAN_SPEC),
373
+ runId: recordIdToString(run.id, TABLES.PLAN_RUN),
374
+ workstreamId: recordIdToString(run.workstreamId, TABLES.WORKSTREAM),
375
+ organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
376
+ title: spec.title,
377
+ objective: spec.objective,
378
+ version: spec.version,
379
+ status: run.status,
380
+ leadAgentId: run.leadAgentId,
381
+ schemaRegistry: structuredClone(spec.schemaRegistry),
382
+ entryNodeIds: [...spec.entryNodeIds],
383
+ edges: [...spec.edges],
384
+ activeNodeIds: run.currentNodeId ? [run.currentNodeId] : [],
385
+ readyNodeIds: [...run.readyNodeIds],
386
+ waitingNodeId: run.waitingNodeId,
387
+ replacedRunId: run.replacedRunId ? recordIdToString(run.replacedRunId, TABLES.PLAN_RUN) : undefined,
388
+ failureCount: run.failureCount,
389
+ startedAt: toOptionalIsoDateTimeString(run.startedAt),
390
+ completedAt: toOptionalIsoDateTimeString(run.completedAt),
391
+ progress: buildProgress(nodeRuns),
392
+ nodes,
393
+ artifacts: artifacts.map(serializeArtifact),
394
+ lineageArtifacts,
395
+ validationIssues: validationIssues.map(serializeValidationIssue),
396
+ approvals: approvals.map(serializeApproval),
397
+ latestCheckpoint: latestCheckpoint ? serializeCheckpoint(latestCheckpoint) : null,
398
+ recentEvents: recentEvents.map(serializeEvent),
399
+ }
400
+ }
401
+
402
+ private async collectLineageArtifacts(run: PlanRunRecord): Promise<SerializablePlanArtifact[]> {
403
+ const lineageArtifacts: SerializablePlanArtifact[] = []
404
+ let currentRunId = run.replacedRunId ? ensureRecordId(run.replacedRunId, TABLES.PLAN_RUN) : null
405
+ let depth = 0
406
+
407
+ while (currentRunId && depth < 5) {
408
+ const previousRun = await databaseService.findOne(TABLES.PLAN_RUN, { id: currentRunId }, PlanRunSchema)
409
+ if (!previousRun) break
410
+
411
+ const artifacts = await this.listArtifacts(previousRun.id)
412
+ lineageArtifacts.unshift(...artifacts.map(serializeArtifact))
413
+
414
+ currentRunId = previousRun.replacedRunId ? ensureRecordId(previousRun.replacedRunId, TABLES.PLAN_RUN) : null
415
+ depth += 1
416
+ }
417
+
418
+ return lineageArtifacts
419
+ }
420
+ }
421
+
422
+ export const planRunService = new PlanRunService()