@lota-sdk/core 0.1.15 → 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.
- package/infrastructure/schema/00_identity.surql +0 -2
- package/infrastructure/schema/01_memory.surql +1 -1
- package/infrastructure/schema/02_execution_plan.surql +62 -1
- package/infrastructure/schema/03_learned_skill.surql +1 -1
- package/infrastructure/schema/06_playbook.surql +25 -0
- package/infrastructure/schema/07_institutional_memory.surql +13 -0
- package/infrastructure/schema/08_quality_metrics.surql +17 -0
- package/package.json +8 -7
- package/src/ai/definitions.ts +80 -2
- package/src/ai/index.ts +0 -2
- package/src/bifrost/bifrost.ts +2 -7
- package/src/config/agent-defaults.ts +31 -21
- package/src/config/agent-types.ts +11 -0
- package/src/config/constants.ts +2 -14
- package/src/config/debug-logger.ts +5 -1
- package/src/config/index.ts +3 -0
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +244 -178
- package/src/db/cursor-pagination.ts +3 -6
- package/src/db/index.ts +2 -0
- package/src/db/memory-store.rows.ts +7 -7
- package/src/db/memory-store.ts +14 -18
- package/src/db/memory.ts +13 -13
- package/src/db/service.ts +153 -79
- package/src/db/startup.ts +6 -10
- package/src/db/surreal-mutation.ts +43 -0
- package/src/db/tables.ts +7 -0
- package/src/db/workstream-message-row.ts +15 -0
- package/src/embeddings/provider.ts +1 -1
- package/src/queues/context-compaction.queue.ts +15 -46
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +16 -51
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +15 -56
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -50
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -52
- package/src/queues/skill-extraction.queue.ts +15 -47
- package/src/queues/workstream-title-generation.queue.ts +15 -47
- package/src/redis/connection.ts +6 -0
- package/src/redis/index.ts +1 -1
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +106 -21
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-runtime.ts +1 -1
- package/src/runtime/context-compaction.ts +22 -60
- package/src/runtime/execution-plan.ts +22 -18
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +9 -197
- package/src/runtime/index.ts +2 -0
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +9 -11
- package/src/runtime/memory-pipeline.ts +6 -9
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +72 -0
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +25 -12
- package/src/runtime/runtime-extensions.ts +2 -2
- package/src/runtime/runtime-worker-registry.ts +6 -0
- package/src/runtime/team-consultation-orchestrator.ts +45 -28
- package/src/runtime/team-consultation-prompts.ts +11 -2
- package/src/runtime/title-helpers.ts +2 -4
- package/src/runtime/workstream-chat-helpers.ts +1 -1
- package/src/services/adaptive-playbook.service.ts +152 -0
- package/src/services/agent-executor.service.ts +293 -0
- package/src/services/artifact-provenance.service.ts +172 -0
- package/src/services/attachment.service.ts +6 -11
- package/src/services/context-compaction.service.ts +72 -55
- package/src/services/context-enrichment.service.ts +33 -0
- package/src/services/coordination-registry.service.ts +117 -0
- package/src/services/document-chunk.service.ts +1 -1
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +269 -50
- package/src/services/feedback-loop.service.ts +96 -0
- package/src/services/global-orchestrator.service.ts +148 -0
- package/src/services/index.ts +26 -0
- package/src/services/institutional-memory.service.ts +145 -0
- package/src/services/learned-skill.service.ts +24 -5
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/memory-utils.ts +3 -8
- package/src/services/memory.service.ts +42 -59
- package/src/services/monitoring-window.service.ts +86 -0
- package/src/services/mutating-approval.service.ts +1 -1
- package/src/services/node-workspace.service.ts +155 -0
- package/src/services/notification.service.ts +39 -0
- package/src/services/organization-member.service.ts +11 -4
- package/src/services/organization.service.ts +5 -5
- package/src/services/ownership-dispatcher.service.ts +403 -0
- package/src/services/plan-approval.service.ts +1 -1
- package/src/services/plan-builder.service.ts +1 -0
- package/src/services/plan-checkpoint.service.ts +30 -2
- package/src/services/plan-compiler.service.ts +5 -0
- package/src/services/plan-coordination.service.ts +152 -0
- package/src/services/plan-cycle.service.ts +284 -0
- package/src/services/plan-deadline.service.ts +287 -0
- package/src/services/plan-executor.service.ts +384 -40
- package/src/services/plan-run.service.ts +41 -7
- package/src/services/plan-scheduler.service.ts +240 -0
- package/src/services/plan-template.service.ts +117 -0
- package/src/services/plan-validator.service.ts +84 -2
- package/src/services/plan-workspace.service.ts +83 -0
- package/src/services/playbook-registry.service.ts +67 -0
- package/src/services/plugin-executor.service.ts +103 -0
- package/src/services/quality-metrics.service.ts +132 -0
- package/src/services/recent-activity.service.ts +27 -31
- package/src/services/skill-resolver.service.ts +19 -0
- package/src/services/system-executor.service.ts +105 -0
- package/src/services/workstream-message.service.ts +12 -34
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -1
- package/src/services/workstream-turn-preparation.service.ts +34 -66
- package/src/services/workstream.service.ts +33 -55
- package/src/services/workstream.types.ts +9 -9
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-utils.ts +1 -1
- package/src/storage/generated-document-storage.service.ts +3 -2
- package/src/system-agents/delegated-agent-factory.ts +2 -0
- package/src/tools/execution-plan.tool.ts +17 -23
- package/src/tools/index.ts +0 -1
- package/src/tools/team-think.tool.ts +6 -4
- package/src/utils/async.ts +2 -1
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +42 -10
- package/src/utils/index.ts +9 -0
- package/src/utils/string.ts +114 -1
- package/src/workers/index.ts +1 -0
- package/src/workers/regular-chat-memory-digest.runner.ts +2 -2
- package/src/workers/skill-extraction.runner.ts +1 -1
- package/src/workers/utils/file-section-chunker.ts +2 -1
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/sandbox-error.ts +11 -2
- package/src/workers/utils/workstream-message-query.ts +11 -20
- package/src/workers/worker-utils.ts +2 -2
- package/src/tools/log-hello-world.tool.ts +0 -17
|
@@ -55,6 +55,8 @@ function buildProgress(nodeRuns: PlanNodeRunRecord[]) {
|
|
|
55
55
|
blocked: 0,
|
|
56
56
|
failed: 0,
|
|
57
57
|
skipped: 0,
|
|
58
|
+
scheduled: 0,
|
|
59
|
+
monitoring: 0,
|
|
58
60
|
} satisfies Record<PlanNodeRunStatus, number>,
|
|
59
61
|
)
|
|
60
62
|
|
|
@@ -72,6 +74,8 @@ function buildProgress(nodeRuns: PlanNodeRunRecord[]) {
|
|
|
72
74
|
blocked: counts.blocked,
|
|
73
75
|
failed: counts.failed,
|
|
74
76
|
skipped: counts.skipped,
|
|
77
|
+
scheduled: counts.scheduled,
|
|
78
|
+
monitoring: counts.monitoring,
|
|
75
79
|
completionRatio: total > 0 ? Number((completedWork / total).toFixed(4)) : undefined,
|
|
76
80
|
}
|
|
77
81
|
}
|
|
@@ -164,8 +168,26 @@ class PlanRunService {
|
|
|
164
168
|
return spec
|
|
165
169
|
}
|
|
166
170
|
|
|
171
|
+
async listPlanSpecsByWorkstream(workstreamId: RecordIdInput): Promise<PlanSpecRecord[]> {
|
|
172
|
+
return databaseService.findMany(
|
|
173
|
+
TABLES.PLAN_SPEC,
|
|
174
|
+
{ workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
|
|
175
|
+
PlanSpecSchema,
|
|
176
|
+
{ orderBy: 'createdAt', orderDir: 'DESC' },
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async listRunsBySpec(planSpecId: RecordIdInput): Promise<PlanRunRecord[]> {
|
|
181
|
+
return databaseService.findMany(
|
|
182
|
+
TABLES.PLAN_RUN,
|
|
183
|
+
{ planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC) },
|
|
184
|
+
PlanRunSchema,
|
|
185
|
+
{ orderBy: 'createdAt', orderDir: 'DESC' },
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
167
189
|
async listNodeSpecs(planSpecId: RecordIdInput): Promise<PlanNodeSpecRecord[]> {
|
|
168
|
-
return
|
|
190
|
+
return databaseService.findMany(
|
|
169
191
|
TABLES.PLAN_NODE_SPEC,
|
|
170
192
|
{ planSpecId: ensureRecordId(planSpecId, TABLES.PLAN_SPEC) },
|
|
171
193
|
PlanNodeSpecRecordSchema,
|
|
@@ -198,6 +220,11 @@ class PlanRunService {
|
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
async getActiveRunRecord(workstreamId: RecordIdInput): Promise<PlanRunRecord | null> {
|
|
223
|
+
const runs = await this.getActiveRunRecords(workstreamId)
|
|
224
|
+
return runs[0] ?? null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async getActiveRunRecords(workstreamId: RecordIdInput): Promise<PlanRunRecord[]> {
|
|
201
228
|
const runs = await databaseService.findMany(
|
|
202
229
|
TABLES.PLAN_RUN,
|
|
203
230
|
{ workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
|
|
@@ -205,11 +232,11 @@ class PlanRunService {
|
|
|
205
232
|
{ orderBy: 'updatedAt', orderDir: 'DESC' },
|
|
206
233
|
)
|
|
207
234
|
|
|
208
|
-
return runs.
|
|
235
|
+
return runs.filter((run) => ACTIVE_RUN_STATUSES.has(run.status))
|
|
209
236
|
}
|
|
210
237
|
|
|
211
238
|
async listNodeRuns(runId: RecordIdInput): Promise<PlanNodeRunRecord[]> {
|
|
212
|
-
return
|
|
239
|
+
return databaseService.findMany(
|
|
213
240
|
TABLES.PLAN_NODE_RUN,
|
|
214
241
|
{ runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
|
|
215
242
|
PlanNodeRunSchema,
|
|
@@ -230,7 +257,7 @@ class PlanRunService {
|
|
|
230
257
|
}
|
|
231
258
|
|
|
232
259
|
async listArtifacts(runId: RecordIdInput): Promise<PlanArtifactRecord[]> {
|
|
233
|
-
return
|
|
260
|
+
return databaseService.findMany(
|
|
234
261
|
TABLES.PLAN_ARTIFACT,
|
|
235
262
|
{ runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
|
|
236
263
|
PlanArtifactSchema,
|
|
@@ -239,7 +266,7 @@ class PlanRunService {
|
|
|
239
266
|
}
|
|
240
267
|
|
|
241
268
|
async listAttempts(runId: RecordIdInput): Promise<PlanNodeAttemptRecord[]> {
|
|
242
|
-
return
|
|
269
|
+
return databaseService.findMany(
|
|
243
270
|
TABLES.PLAN_NODE_ATTEMPT,
|
|
244
271
|
{ runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
|
|
245
272
|
PlanNodeAttemptSchema,
|
|
@@ -257,14 +284,14 @@ class PlanRunService {
|
|
|
257
284
|
if (params.planSpecId) filter.planSpecId = ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC)
|
|
258
285
|
if (params.attemptId) filter.attemptId = ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT)
|
|
259
286
|
|
|
260
|
-
return
|
|
287
|
+
return databaseService.findMany(TABLES.PLAN_VALIDATION_ISSUE, filter, PlanValidationIssueSchema, {
|
|
261
288
|
orderBy: 'createdAt',
|
|
262
289
|
orderDir: 'ASC',
|
|
263
290
|
})
|
|
264
291
|
}
|
|
265
292
|
|
|
266
293
|
async listApprovals(runId: RecordIdInput): Promise<PlanApprovalRecord[]> {
|
|
267
|
-
return
|
|
294
|
+
return databaseService.findMany(
|
|
268
295
|
TABLES.PLAN_APPROVAL,
|
|
269
296
|
{ runId: ensureRecordId(runId, TABLES.PLAN_RUN) },
|
|
270
297
|
PlanApprovalSchema,
|
|
@@ -352,6 +379,11 @@ class PlanRunService {
|
|
|
352
379
|
attachmentPolicy: nodeSpec.contextPolicy.attachmentPolicy,
|
|
353
380
|
webPolicy: nodeSpec.contextPolicy.webPolicy,
|
|
354
381
|
},
|
|
382
|
+
schedule: nodeSpec.schedule,
|
|
383
|
+
deadline: nodeSpec.deadline,
|
|
384
|
+
monitoringConfig: nodeSpec.monitoringConfig,
|
|
385
|
+
delayAfterPredecessorMs: nodeSpec.delayAfterPredecessorMs,
|
|
386
|
+
deliberationConfig: nodeSpec.deliberationConfig,
|
|
355
387
|
status: nodeRun.status,
|
|
356
388
|
attemptCount: nodeRun.attemptCount,
|
|
357
389
|
retryCount: nodeRun.retryCount,
|
|
@@ -378,9 +410,11 @@ class PlanRunService {
|
|
|
378
410
|
version: spec.version,
|
|
379
411
|
status: run.status,
|
|
380
412
|
leadAgentId: run.leadAgentId,
|
|
413
|
+
executionMode: spec.executionMode,
|
|
381
414
|
schemaRegistry: structuredClone(spec.schemaRegistry),
|
|
382
415
|
entryNodeIds: [...spec.entryNodeIds],
|
|
383
416
|
edges: [...spec.edges],
|
|
417
|
+
schedule: spec.schedule,
|
|
384
418
|
activeNodeIds: run.currentNodeId ? [run.currentNodeId] : [],
|
|
385
419
|
readyNodeIds: [...run.readyNodeIds],
|
|
386
420
|
waitingNodeId: run.waitingNodeId,
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { PlanCycleRecordSchema, PlanScheduleRecordSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { PlanScheduleRecord, PlanScheduleSpec } from '@lota-sdk/shared'
|
|
3
|
+
import { CronExpressionParser } from 'cron-parser'
|
|
4
|
+
|
|
5
|
+
import type { RecordIdInput } from '../db/record-id'
|
|
6
|
+
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
7
|
+
import { databaseService } from '../db/service'
|
|
8
|
+
import { TABLES } from '../db/tables'
|
|
9
|
+
import { toDatabaseDateTime } from '../utils/date-time'
|
|
10
|
+
|
|
11
|
+
class PlanSchedulerService {
|
|
12
|
+
computeNextFireAt(spec: PlanScheduleSpec, baseTime: Date = new Date()): Date | null {
|
|
13
|
+
switch (spec.type) {
|
|
14
|
+
case 'immediate':
|
|
15
|
+
return baseTime
|
|
16
|
+
|
|
17
|
+
case 'absolute':
|
|
18
|
+
return spec.at ? new Date(spec.at) : null
|
|
19
|
+
|
|
20
|
+
case 'relative':
|
|
21
|
+
return spec.delayMs !== undefined ? new Date(baseTime.getTime() + spec.delayMs) : null
|
|
22
|
+
|
|
23
|
+
case 'cron': {
|
|
24
|
+
if (!spec.cron) return null
|
|
25
|
+
try {
|
|
26
|
+
const expr = CronExpressionParser.parse(spec.cron, { currentDate: baseTime })
|
|
27
|
+
const next = expr.next()
|
|
28
|
+
return next.toDate()
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
case 'monitoring':
|
|
35
|
+
return spec.intervalMs !== undefined ? new Date(baseTime.getTime() + spec.intervalMs) : null
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async createSchedule(params: {
|
|
43
|
+
organizationId: RecordIdInput
|
|
44
|
+
workstreamId: RecordIdInput
|
|
45
|
+
planSpecId?: RecordIdInput
|
|
46
|
+
runId?: RecordIdInput
|
|
47
|
+
nodeId?: string
|
|
48
|
+
scheduleSpec: PlanScheduleSpec
|
|
49
|
+
}): Promise<PlanScheduleRecord> {
|
|
50
|
+
const nextFireAt = this.computeNextFireAt(params.scheduleSpec)
|
|
51
|
+
const now = new Date()
|
|
52
|
+
|
|
53
|
+
const record = await databaseService.create(
|
|
54
|
+
TABLES.PLAN_SCHEDULE,
|
|
55
|
+
{
|
|
56
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
57
|
+
workstreamId: ensureRecordId(params.workstreamId, TABLES.WORKSTREAM),
|
|
58
|
+
planSpecId: params.planSpecId ? ensureRecordId(params.planSpecId, TABLES.PLAN_SPEC) : undefined,
|
|
59
|
+
runId: params.runId ? ensureRecordId(params.runId, TABLES.PLAN_RUN) : undefined,
|
|
60
|
+
nodeId: params.nodeId,
|
|
61
|
+
scheduleSpec: params.scheduleSpec,
|
|
62
|
+
status: 'active',
|
|
63
|
+
fireCount: 0,
|
|
64
|
+
...(nextFireAt ? { nextFireAt: toDatabaseDateTime(nextFireAt) } : {}),
|
|
65
|
+
createdAt: now,
|
|
66
|
+
},
|
|
67
|
+
PlanScheduleRecordSchema,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Enqueue a delayed BullMQ job to fire at the scheduled time
|
|
71
|
+
if (nextFireAt) {
|
|
72
|
+
const delay = Math.max(0, nextFireAt.getTime() - Date.now())
|
|
73
|
+
const { enqueueScheduleFire } = await import('../queues/plan-scheduler.queue')
|
|
74
|
+
await enqueueScheduleFire(recordIdToString(record.id, TABLES.PLAN_SCHEDULE), delay)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return record
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Called by the BullMQ worker when a fire-schedule job executes. */
|
|
81
|
+
async fireScheduleById(scheduleId: string): Promise<void> {
|
|
82
|
+
const schedule = await databaseService.findOne(
|
|
83
|
+
TABLES.PLAN_SCHEDULE,
|
|
84
|
+
{ id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
|
|
85
|
+
PlanScheduleRecordSchema,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if (!schedule || schedule.status !== 'active') return
|
|
89
|
+
|
|
90
|
+
await this.fireSchedule(schedule)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async fireSchedule(schedule: PlanScheduleRecord): Promise<void> {
|
|
94
|
+
const now = new Date()
|
|
95
|
+
const newFireCount = schedule.fireCount + 1
|
|
96
|
+
const isRecurring = schedule.scheduleSpec.type === 'cron' || schedule.scheduleSpec.type === 'monitoring'
|
|
97
|
+
const maxReached = schedule.scheduleSpec.maxFires !== undefined && newFireCount >= schedule.scheduleSpec.maxFires
|
|
98
|
+
|
|
99
|
+
let nextFireAt: Date | null = null
|
|
100
|
+
let newStatus: 'active' | 'completed' = 'completed'
|
|
101
|
+
|
|
102
|
+
if (isRecurring && !maxReached) {
|
|
103
|
+
nextFireAt = this.computeNextFireAt(schedule.scheduleSpec, now)
|
|
104
|
+
newStatus = nextFireAt ? 'active' : 'completed'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await databaseService.update(
|
|
108
|
+
TABLES.PLAN_SCHEDULE,
|
|
109
|
+
schedule.id,
|
|
110
|
+
{ fireCount: newFireCount, lastFiredAt: now, nextFireAt: toDatabaseDateTime(nextFireAt), status: newStatus },
|
|
111
|
+
PlanScheduleRecordSchema,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// Enqueue next fire for recurring schedules
|
|
115
|
+
if (newStatus === 'active' && nextFireAt) {
|
|
116
|
+
const delay = Math.max(0, nextFireAt.getTime() - Date.now())
|
|
117
|
+
const scheduleIdStr = recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)
|
|
118
|
+
const { enqueueScheduleFire } = await import('../queues/plan-scheduler.queue')
|
|
119
|
+
await enqueueScheduleFire(scheduleIdStr, delay)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let shouldRecoverDeadlineChecks = false
|
|
123
|
+
|
|
124
|
+
// Dispatch: node-level schedule — promote the delayed node
|
|
125
|
+
if (schedule.runId && schedule.nodeId) {
|
|
126
|
+
const { planExecutorService } = await import('./plan-executor.service')
|
|
127
|
+
await planExecutorService.promoteDelayedNode({
|
|
128
|
+
runId: recordIdToString(schedule.runId, TABLES.PLAN_RUN),
|
|
129
|
+
nodeId: schedule.nodeId,
|
|
130
|
+
emittedBy: 'plan-scheduler',
|
|
131
|
+
})
|
|
132
|
+
shouldRecoverDeadlineChecks = true
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Dispatch: plan-level schedule — advance a linked cycle or create a new run
|
|
136
|
+
if (schedule.planSpecId && !schedule.runId) {
|
|
137
|
+
const cycle = await databaseService.findOne(
|
|
138
|
+
TABLES.PLAN_CYCLE,
|
|
139
|
+
{ scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE) },
|
|
140
|
+
PlanCycleRecordSchema,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if (cycle) {
|
|
144
|
+
const { planCycleService } = await import('./plan-cycle.service')
|
|
145
|
+
await planCycleService.advanceCycle(cycle.id)
|
|
146
|
+
shouldRecoverDeadlineChecks = true
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (shouldRecoverDeadlineChecks) {
|
|
151
|
+
const { planDeadlineService } = await import('./plan-deadline.service')
|
|
152
|
+
await planDeadlineService.recoverDeadlineChecks()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Re-enqueue BullMQ jobs for all active schedules. Called once at worker startup. */
|
|
157
|
+
async recoverActiveSchedules(): Promise<void> {
|
|
158
|
+
const activeSchedules = await databaseService.queryMany(
|
|
159
|
+
{
|
|
160
|
+
query: `SELECT * FROM ${TABLES.PLAN_SCHEDULE} WHERE status = $status ORDER BY nextFireAt ASC`,
|
|
161
|
+
bindings: { status: 'active' },
|
|
162
|
+
},
|
|
163
|
+
PlanScheduleRecordSchema,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const { enqueueScheduleFire } = await import('../queues/plan-scheduler.queue')
|
|
167
|
+
|
|
168
|
+
for (const schedule of activeSchedules) {
|
|
169
|
+
if (!schedule.nextFireAt) continue
|
|
170
|
+
const scheduleId = recordIdToString(schedule.id, TABLES.PLAN_SCHEDULE)
|
|
171
|
+
const delay = Math.max(0, new Date(schedule.nextFireAt).getTime() - Date.now())
|
|
172
|
+
await enqueueScheduleFire(scheduleId, delay)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async cancelSchedule(scheduleId: RecordIdInput): Promise<void> {
|
|
177
|
+
const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
|
|
178
|
+
const { removeScheduleFireJob } = await import('../queues/plan-scheduler.queue')
|
|
179
|
+
await removeScheduleFireJob(idStr)
|
|
180
|
+
|
|
181
|
+
await databaseService.update(
|
|
182
|
+
TABLES.PLAN_SCHEDULE,
|
|
183
|
+
ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
184
|
+
{ status: 'cancelled' },
|
|
185
|
+
PlanScheduleRecordSchema,
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async pauseSchedule(scheduleId: RecordIdInput): Promise<void> {
|
|
190
|
+
const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
|
|
191
|
+
const { removeScheduleFireJob } = await import('../queues/plan-scheduler.queue')
|
|
192
|
+
await removeScheduleFireJob(idStr)
|
|
193
|
+
|
|
194
|
+
await databaseService.update(
|
|
195
|
+
TABLES.PLAN_SCHEDULE,
|
|
196
|
+
ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
197
|
+
{ status: 'paused' },
|
|
198
|
+
PlanScheduleRecordSchema,
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async resumeSchedule(scheduleId: RecordIdInput): Promise<void> {
|
|
203
|
+
const schedule = await databaseService.findOne(
|
|
204
|
+
TABLES.PLAN_SCHEDULE,
|
|
205
|
+
{ id: ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE) },
|
|
206
|
+
PlanScheduleRecordSchema,
|
|
207
|
+
)
|
|
208
|
+
if (!schedule) {
|
|
209
|
+
throw new Error(`Schedule not found: ${recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const nextFireAt = this.computeNextFireAt(schedule.scheduleSpec)
|
|
213
|
+
|
|
214
|
+
await databaseService.update(
|
|
215
|
+
TABLES.PLAN_SCHEDULE,
|
|
216
|
+
ensureRecordId(scheduleId, TABLES.PLAN_SCHEDULE),
|
|
217
|
+
{ status: 'active', nextFireAt: toDatabaseDateTime(nextFireAt) },
|
|
218
|
+
PlanScheduleRecordSchema,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Re-enqueue the delayed fire job
|
|
222
|
+
if (nextFireAt) {
|
|
223
|
+
const delay = Math.max(0, nextFireAt.getTime() - Date.now())
|
|
224
|
+
const idStr = recordIdToString(scheduleId, TABLES.PLAN_SCHEDULE)
|
|
225
|
+
const { enqueueScheduleFire } = await import('../queues/plan-scheduler.queue')
|
|
226
|
+
await enqueueScheduleFire(idStr, delay)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async listSchedules(workstreamId: RecordIdInput): Promise<PlanScheduleRecord[]> {
|
|
231
|
+
return databaseService.findMany(
|
|
232
|
+
TABLES.PLAN_SCHEDULE,
|
|
233
|
+
{ workstreamId: ensureRecordId(workstreamId, TABLES.WORKSTREAM) },
|
|
234
|
+
PlanScheduleRecordSchema,
|
|
235
|
+
{ orderBy: 'createdAt', orderDir: 'ASC' },
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const planSchedulerService = new PlanSchedulerService()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { PlanTemplateRecordSchema } from '@lota-sdk/shared'
|
|
2
|
+
import type { ExecutionPlanToolResultData, PlanArtifactRecord, PlanDraft, PlanTemplateRecord } from '@lota-sdk/shared'
|
|
3
|
+
|
|
4
|
+
import type { RecordIdInput } from '../db/record-id'
|
|
5
|
+
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
6
|
+
import { databaseService } from '../db/service'
|
|
7
|
+
import { TABLES } from '../db/tables'
|
|
8
|
+
import { executionPlanService } from './execution-plan.service'
|
|
9
|
+
|
|
10
|
+
class PlanTemplateService {
|
|
11
|
+
async createTemplate(params: {
|
|
12
|
+
organizationId: RecordIdInput
|
|
13
|
+
name: string
|
|
14
|
+
description?: string
|
|
15
|
+
draft: PlanDraft
|
|
16
|
+
tags?: string[]
|
|
17
|
+
source?: 'user' | 'playbook' | 'system'
|
|
18
|
+
sourceRef?: string
|
|
19
|
+
}): Promise<PlanTemplateRecord> {
|
|
20
|
+
const now = new Date()
|
|
21
|
+
return databaseService.create(
|
|
22
|
+
TABLES.PLAN_TEMPLATE,
|
|
23
|
+
{
|
|
24
|
+
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
25
|
+
name: params.name,
|
|
26
|
+
...(params.description ? { description: params.description } : {}),
|
|
27
|
+
draft: params.draft,
|
|
28
|
+
tags: params.tags ?? [],
|
|
29
|
+
source: params.source ?? 'user',
|
|
30
|
+
...(params.sourceRef ? { sourceRef: params.sourceRef } : {}),
|
|
31
|
+
createdAt: now,
|
|
32
|
+
},
|
|
33
|
+
PlanTemplateRecordSchema,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getTemplate(templateId: RecordIdInput): Promise<PlanTemplateRecord | null> {
|
|
38
|
+
return databaseService.findOne(
|
|
39
|
+
TABLES.PLAN_TEMPLATE,
|
|
40
|
+
{ id: ensureRecordId(templateId, TABLES.PLAN_TEMPLATE) },
|
|
41
|
+
PlanTemplateRecordSchema,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async listTemplates(
|
|
46
|
+
organizationId: RecordIdInput,
|
|
47
|
+
params?: { tags?: string[]; source?: string },
|
|
48
|
+
): Promise<PlanTemplateRecord[]> {
|
|
49
|
+
const filter: Record<string, unknown> = { organizationId: ensureRecordId(organizationId, TABLES.ORGANIZATION) }
|
|
50
|
+
if (params?.source) {
|
|
51
|
+
filter.source = params.source
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const templates = await databaseService.findMany(TABLES.PLAN_TEMPLATE, filter, PlanTemplateRecordSchema, {
|
|
55
|
+
orderBy: 'createdAt',
|
|
56
|
+
orderDir: 'ASC',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (params?.tags && params.tags.length > 0) {
|
|
60
|
+
const tagSet = new Set(params.tags)
|
|
61
|
+
return templates.filter((t) => t.tags.some((tag) => tagSet.has(tag)))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return templates
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async updateTemplate(
|
|
68
|
+
templateId: RecordIdInput,
|
|
69
|
+
patch: Partial<{ name: string; description: string; draft: PlanDraft; tags: string[] }>,
|
|
70
|
+
): Promise<PlanTemplateRecord> {
|
|
71
|
+
const updated = await databaseService.update(
|
|
72
|
+
TABLES.PLAN_TEMPLATE,
|
|
73
|
+
ensureRecordId(templateId, TABLES.PLAN_TEMPLATE),
|
|
74
|
+
patch,
|
|
75
|
+
PlanTemplateRecordSchema,
|
|
76
|
+
)
|
|
77
|
+
if (!updated) {
|
|
78
|
+
throw new Error(`Template not found: ${recordIdToString(templateId, TABLES.PLAN_TEMPLATE)}`)
|
|
79
|
+
}
|
|
80
|
+
return updated
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async deleteTemplate(templateId: RecordIdInput): Promise<void> {
|
|
84
|
+
await databaseService.deleteById(TABLES.PLAN_TEMPLATE, ensureRecordId(templateId, TABLES.PLAN_TEMPLATE))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async instantiate(params: {
|
|
88
|
+
templateId: RecordIdInput
|
|
89
|
+
organizationId: RecordIdInput
|
|
90
|
+
workstreamId: RecordIdInput
|
|
91
|
+
leadAgentId: string
|
|
92
|
+
overrides?: Partial<PlanDraft>
|
|
93
|
+
carryForwardArtifacts?: PlanArtifactRecord[]
|
|
94
|
+
}): Promise<ExecutionPlanToolResultData> {
|
|
95
|
+
const template = await this.getTemplate(params.templateId)
|
|
96
|
+
if (!template) {
|
|
97
|
+
throw new Error(`Template not found: ${recordIdToString(params.templateId, TABLES.PLAN_TEMPLATE)}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const draft: PlanDraft = { ...template.draft, ...params.overrides }
|
|
101
|
+
|
|
102
|
+
if (params.carryForwardArtifacts && params.carryForwardArtifacts.length > 0) {
|
|
103
|
+
const carryContext = params.carryForwardArtifacts.map((a) => `[carry-forward] ${a.name}: ${a.pointer}`)
|
|
104
|
+
draft.objective = `${draft.objective}\n\nCarry-forward context:\n${carryContext.join('\n')}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return executionPlanService.createPlan({
|
|
108
|
+
organizationId: params.organizationId,
|
|
109
|
+
workstreamId: params.workstreamId,
|
|
110
|
+
leadAgentId: params.leadAgentId,
|
|
111
|
+
dispatchMode: 'stable-boundary',
|
|
112
|
+
input: draft,
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const planTemplateService = new PlanTemplateService()
|
|
@@ -9,9 +9,10 @@ import type {
|
|
|
9
9
|
} from '@lota-sdk/shared'
|
|
10
10
|
|
|
11
11
|
import { isRecord } from '../utils/string'
|
|
12
|
+
import { planCoordinationService } from './plan-coordination.service'
|
|
12
13
|
import { readPathValue } from './plan-helpers'
|
|
13
14
|
|
|
14
|
-
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
15
|
+
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
|
|
15
16
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
16
17
|
|
|
17
18
|
export interface PlanValidationIssueInput {
|
|
@@ -54,7 +55,7 @@ function hasAllFields(value: unknown, fields: string[]): boolean {
|
|
|
54
55
|
return fields.every((field) => readPathValue(value, field) !== undefined)
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function validateSchemaValue(params: { schema: PlanDataSchema; value: unknown; path: string }): string[] {
|
|
58
|
+
export function validateSchemaValue(params: { schema: PlanDataSchema; value: unknown; path: string }): string[] {
|
|
58
59
|
const issues: string[] = []
|
|
59
60
|
const { schema, value, path } = params
|
|
60
61
|
|
|
@@ -236,6 +237,24 @@ class PlanValidatorService {
|
|
|
236
237
|
}),
|
|
237
238
|
)
|
|
238
239
|
}
|
|
240
|
+
if (node.owner.executorType === 'plugin' && node.owner.operation.trim().length === 0) {
|
|
241
|
+
blocking.push(
|
|
242
|
+
createIssue({
|
|
243
|
+
code: 'plugin_owner_operation_missing',
|
|
244
|
+
message: `Plugin-owned node "${node.label}" must declare an operation.`,
|
|
245
|
+
nodeId: node.id,
|
|
246
|
+
}),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
if (node.owner.executorType === 'system' && node.owner.operation.trim().length === 0) {
|
|
250
|
+
blocking.push(
|
|
251
|
+
createIssue({
|
|
252
|
+
code: 'system_owner_operation_missing',
|
|
253
|
+
message: `System-owned node "${node.label}" must declare an operation.`,
|
|
254
|
+
nodeId: node.id,
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
}
|
|
239
258
|
if (node.inputSchemaRef && !resolveSchemaRef(draft, node.inputSchemaRef)) {
|
|
240
259
|
blocking.push(
|
|
241
260
|
createIssue({
|
|
@@ -289,6 +308,61 @@ class PlanValidatorService {
|
|
|
289
308
|
}),
|
|
290
309
|
)
|
|
291
310
|
}
|
|
311
|
+
|
|
312
|
+
// Validate deliberation-fork nodes
|
|
313
|
+
if (node.type === 'deliberation-fork') {
|
|
314
|
+
if (!node.deliberationConfig) {
|
|
315
|
+
blocking.push(
|
|
316
|
+
createIssue({
|
|
317
|
+
code: 'deliberation_fork_missing_config',
|
|
318
|
+
message: `Deliberation-fork node "${node.label}" must define deliberationConfig.`,
|
|
319
|
+
nodeId: node.id,
|
|
320
|
+
}),
|
|
321
|
+
)
|
|
322
|
+
} else {
|
|
323
|
+
if (node.deliberationConfig.branches.length < 2) {
|
|
324
|
+
blocking.push(
|
|
325
|
+
createIssue({
|
|
326
|
+
code: 'deliberation_fork_insufficient_branches',
|
|
327
|
+
message: `Deliberation-fork node "${node.label}" must define at least 2 branches.`,
|
|
328
|
+
nodeId: node.id,
|
|
329
|
+
}),
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
for (const branch of node.deliberationConfig.branches) {
|
|
333
|
+
if (!nodesById.has(branch.entryNodeId)) {
|
|
334
|
+
blocking.push(
|
|
335
|
+
createIssue({
|
|
336
|
+
code: 'deliberation_fork_missing_branch_entry',
|
|
337
|
+
message: `Deliberation-fork node "${node.label}" references missing branch entry node "${branch.entryNodeId}".`,
|
|
338
|
+
nodeId: node.id,
|
|
339
|
+
detail: { branchId: branch.branchId, entryNodeId: branch.entryNodeId },
|
|
340
|
+
}),
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const gateNode = nodesById.get(node.deliberationConfig.resolutionGateNodeId)
|
|
345
|
+
if (!gateNode) {
|
|
346
|
+
blocking.push(
|
|
347
|
+
createIssue({
|
|
348
|
+
code: 'deliberation_fork_missing_gate',
|
|
349
|
+
message: `Deliberation-fork node "${node.label}" references missing resolution gate node "${node.deliberationConfig.resolutionGateNodeId}".`,
|
|
350
|
+
nodeId: node.id,
|
|
351
|
+
detail: { resolutionGateNodeId: node.deliberationConfig.resolutionGateNodeId },
|
|
352
|
+
}),
|
|
353
|
+
)
|
|
354
|
+
} else if (gateNode.type !== 'human-decision') {
|
|
355
|
+
blocking.push(
|
|
356
|
+
createIssue({
|
|
357
|
+
code: 'deliberation_fork_gate_type_mismatch',
|
|
358
|
+
message: `Resolution gate node "${gateNode.label}" must be of type "human-decision".`,
|
|
359
|
+
nodeId: node.id,
|
|
360
|
+
detail: { gateNodeId: gateNode.id, gateNodeType: gateNode.type },
|
|
361
|
+
}),
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
292
366
|
}
|
|
293
367
|
|
|
294
368
|
const entryNodeIds = draft.entryNodeIds ?? []
|
|
@@ -431,6 +505,14 @@ class PlanValidatorService {
|
|
|
431
505
|
)
|
|
432
506
|
}
|
|
433
507
|
|
|
508
|
+
// Validate cross-plan dependency cycles
|
|
509
|
+
if (draft.dependencies && draft.dependencies.length > 0) {
|
|
510
|
+
const cycleIssues = planCoordinationService.validateNoCycles([
|
|
511
|
+
{ title: draft.title, dependencies: draft.dependencies },
|
|
512
|
+
])
|
|
513
|
+
blocking.push(...cycleIssues)
|
|
514
|
+
}
|
|
515
|
+
|
|
434
516
|
return { blocking, warnings }
|
|
435
517
|
}
|
|
436
518
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { getRedisConnection } from '../redis'
|
|
4
|
+
|
|
5
|
+
const PlanWorkspaceEntrySchema = z.object({
|
|
6
|
+
value: z.unknown(),
|
|
7
|
+
version: z.number(),
|
|
8
|
+
writeSequence: z.number(),
|
|
9
|
+
nodeId: z.string(),
|
|
10
|
+
timestamp: z.number(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export type PlanWorkspaceEntry = z.infer<typeof PlanWorkspaceEntrySchema>
|
|
14
|
+
|
|
15
|
+
class PlanWorkspaceService {
|
|
16
|
+
private keyPrefix = 'plan-workspace:'
|
|
17
|
+
|
|
18
|
+
async write(params: {
|
|
19
|
+
runId: string
|
|
20
|
+
nodeId: string
|
|
21
|
+
key: string
|
|
22
|
+
value: unknown
|
|
23
|
+
version: number
|
|
24
|
+
checkpointSequence: number
|
|
25
|
+
}): Promise<void> {
|
|
26
|
+
const redis = getRedisConnection()
|
|
27
|
+
const hashKey = `${this.keyPrefix}${params.runId}`
|
|
28
|
+
const fieldKey = `${params.nodeId}:${params.key}`
|
|
29
|
+
const entry: PlanWorkspaceEntry = {
|
|
30
|
+
value: params.value,
|
|
31
|
+
version: params.version,
|
|
32
|
+
writeSequence: params.checkpointSequence,
|
|
33
|
+
nodeId: params.nodeId,
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
}
|
|
36
|
+
await redis.hset(hashKey, fieldKey, JSON.stringify(entry))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async snapshotRead(params: {
|
|
40
|
+
runId: string
|
|
41
|
+
readerNodeId: string
|
|
42
|
+
snapshotSequence?: number
|
|
43
|
+
}): Promise<Record<string, PlanWorkspaceEntry>> {
|
|
44
|
+
const redis = getRedisConnection()
|
|
45
|
+
const hashKey = `${this.keyPrefix}${params.runId}`
|
|
46
|
+
const nodePrefix = `${params.readerNodeId}:`
|
|
47
|
+
const all = await redis.hgetall(hashKey)
|
|
48
|
+
const result: Record<string, PlanWorkspaceEntry> = {}
|
|
49
|
+
for (const [fieldKey, raw] of Object.entries(all)) {
|
|
50
|
+
if (!fieldKey.startsWith(nodePrefix)) continue
|
|
51
|
+
const entry = PlanWorkspaceEntrySchema.parse(JSON.parse(raw))
|
|
52
|
+
if (params.snapshotSequence !== undefined && entry.writeSequence > params.snapshotSequence) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
result[fieldKey] = entry
|
|
56
|
+
}
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async currentRead(params: { runId: string; key?: string }): Promise<Record<string, PlanWorkspaceEntry>> {
|
|
61
|
+
const redis = getRedisConnection()
|
|
62
|
+
const hashKey = `${this.keyPrefix}${params.runId}`
|
|
63
|
+
if (params.key) {
|
|
64
|
+
// Support both raw field keys (e.g. 'nodeId:key') and plain keys
|
|
65
|
+
const raw = await redis.hget(hashKey, params.key)
|
|
66
|
+
if (!raw) return {}
|
|
67
|
+
return { [params.key]: PlanWorkspaceEntrySchema.parse(JSON.parse(raw)) }
|
|
68
|
+
}
|
|
69
|
+
const all = await redis.hgetall(hashKey)
|
|
70
|
+
const result: Record<string, PlanWorkspaceEntry> = {}
|
|
71
|
+
for (const [k, v] of Object.entries(all)) {
|
|
72
|
+
result[k] = PlanWorkspaceEntrySchema.parse(JSON.parse(v))
|
|
73
|
+
}
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async cleanup(runId: string): Promise<void> {
|
|
78
|
+
const redis = getRedisConnection()
|
|
79
|
+
await redis.del(`${this.keyPrefix}${runId}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const planWorkspaceService = new PlanWorkspaceService()
|