@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.
- 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 +9 -8
- package/src/ai/definitions.ts +80 -2
- package/src/ai/embedding-cache.ts +7 -6
- package/src/ai/index.ts +0 -1
- package/src/bifrost/bifrost.ts +14 -14
- package/src/config/agent-defaults.ts +32 -22
- 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/logger.ts +7 -9
- package/src/config/model-constants.ts +16 -34
- package/src/config/search.ts +1 -15
- package/src/create-runtime.ts +453 -0
- 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 +24 -24
- package/src/db/memory.ts +18 -16
- package/src/db/schema-fingerprint.ts +1 -0
- package/src/db/service.ts +193 -122
- package/src/db/startup.ts +9 -13
- 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/index.ts +1 -1
- package/src/queues/context-compaction.queue.ts +17 -52
- package/src/queues/delayed-node-promotion.queue.ts +41 -0
- package/src/queues/document-processor.queue.ts +7 -7
- package/src/queues/index.ts +3 -0
- package/src/queues/memory-consolidation.queue.ts +18 -54
- package/src/queues/plan-scheduler.queue.ts +97 -0
- package/src/queues/post-chat-memory.queue.ts +15 -60
- package/src/queues/queue-factory.ts +100 -0
- package/src/queues/recent-activity-title-refinement.queue.ts +15 -54
- package/src/queues/regular-chat-memory-digest.queue.ts +16 -55
- package/src/queues/skill-extraction.queue.ts +15 -50
- package/src/queues/workstream-title-generation.queue.ts +15 -51
- package/src/redis/connection.ts +12 -3
- package/src/redis/index.ts +2 -1
- package/src/redis/org-memory-lock.ts +1 -1
- package/src/redis/redis-lease-lock.ts +41 -8
- package/src/redis/stream-context.ts +11 -0
- package/src/runtime/agent-runtime-policy.ts +106 -21
- package/src/runtime/agent-stream-helpers.ts +2 -1
- package/src/runtime/approval-continuation.ts +12 -6
- package/src/runtime/context-compaction-constants.ts +1 -1
- package/src/runtime/context-compaction-runtime.ts +7 -5
- package/src/runtime/context-compaction.ts +40 -97
- package/src/runtime/execution-plan.ts +23 -19
- package/src/runtime/graph-designer.ts +15 -0
- package/src/runtime/helper-model.ts +10 -196
- package/src/runtime/index.ts +14 -1
- package/src/runtime/llm-content.ts +1 -1
- package/src/runtime/memory-block.ts +11 -12
- package/src/runtime/memory-pipeline.ts +26 -10
- package/src/runtime/plugin-resolution.ts +35 -0
- package/src/runtime/plugin-types.ts +73 -1
- package/src/runtime/retrieval-adapters.ts +1 -1
- package/src/runtime/runtime-config.ts +25 -12
- package/src/runtime/runtime-extensions.ts +91 -15
- 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 +11 -4
- package/src/runtime/workstream-chat-helpers.ts +6 -7
- package/src/runtime/workstream-routing-policy.ts +0 -30
- package/src/runtime/workstream-state.ts +17 -7
- 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 +7 -12
- package/src/services/context-compaction.service.ts +75 -58
- 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 +38 -33
- package/src/services/domain-agent-executor.service.ts +71 -0
- package/src/services/execution-plan.service.ts +271 -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 +30 -15
- package/src/services/memory-assessment.service.ts +3 -2
- package/src/services/{memory.utils.ts → memory-utils.ts} +4 -13
- package/src/services/memory.service.ts +55 -69
- 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 +12 -5
- 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-artifact.service.ts +1 -0
- 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 +386 -58
- package/src/services/plan-helpers.ts +15 -0
- 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 +87 -20
- 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-title.service.ts +3 -10
- package/src/services/recent-activity.service.ts +33 -43
- 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 +29 -41
- package/src/services/workstream-plan-registry.service.ts +22 -0
- package/src/services/workstream-title.service.ts +3 -9
- package/src/services/{workstream-turn-preparation.ts → workstream-turn-preparation.service.ts} +428 -373
- package/src/services/workstream-turn.ts +2 -2
- package/src/services/workstream.service.ts +55 -65
- package/src/services/workstream.types.ts +10 -19
- package/src/services/write-intent-validator.service.ts +81 -0
- package/src/storage/attachment-parser.ts +1 -1
- package/src/storage/attachment-storage.service.ts +4 -4
- package/src/storage/{attachments.utils.ts → attachment-utils.ts} +2 -5
- package/src/storage/generated-document-storage.service.ts +3 -2
- package/src/storage/index.ts +2 -2
- package/src/system-agents/{context-compacter.agent.ts → context-compaction.agent.ts} +4 -4
- package/src/system-agents/delegated-agent-factory.ts +5 -2
- package/src/system-agents/index.ts +8 -0
- package/src/system-agents/memory-reranker.agent.ts +1 -1
- package/src/system-agents/memory.agent.ts +1 -1
- package/src/system-agents/recent-activity-title-refiner.agent.ts +1 -1
- package/src/tools/execution-plan.tool.ts +17 -19
- package/src/tools/fetch-webpage.tool.ts +20 -18
- package/src/tools/index.ts +2 -3
- package/src/tools/read-file-parts.tool.ts +1 -1
- package/src/tools/search-web.tool.ts +18 -15
- package/src/tools/{search-tools.ts → search.tool.ts} +1 -1
- package/src/tools/team-think.tool.ts +14 -8
- package/src/tools/{tool-contract.ts → tool-contracts.ts} +9 -2
- package/src/utils/async.ts +3 -2
- package/src/utils/date-time.ts +4 -32
- package/src/utils/env.ts +8 -0
- package/src/utils/errors.ts +47 -0
- package/src/utils/hono-error-handler.ts +1 -2
- package/src/utils/index.ts +19 -2
- package/src/utils/string.ts +128 -1
- package/src/workers/bootstrap.ts +2 -2
- package/src/workers/index.ts +1 -0
- package/src/workers/memory-consolidation.worker.ts +12 -12
- package/src/workers/regular-chat-memory-digest.helpers.ts +2 -7
- package/src/workers/regular-chat-memory-digest.runner.ts +11 -105
- package/src/workers/skill-extraction.runner.ts +8 -102
- package/src/workers/utils/file-section-chunker.ts +6 -3
- 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 +97 -0
- package/src/workers/worker-utils.ts +6 -2
- package/src/runtime/retrieval-pipeline.ts +0 -3
- package/src/runtime.ts +0 -387
- package/src/tools/log-hello-world.tool.ts +0 -17
- package/src/utils/error.ts +0 -10
- /package/src/services/{context-compaction-runtime.ts → context-compaction-runtime.singleton.ts} +0 -0
- /package/src/storage/{attachments.types.ts → attachment-types.ts} +0 -0
|
@@ -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()
|
|
@@ -8,7 +8,11 @@ import type {
|
|
|
8
8
|
PlanValidationIssueSeverity,
|
|
9
9
|
} from '@lota-sdk/shared'
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import { isRecord } from '../utils/string'
|
|
12
|
+
import { planCoordinationService } from './plan-coordination.service'
|
|
13
|
+
import { readPathValue } from './plan-helpers'
|
|
14
|
+
|
|
15
|
+
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
|
|
12
16
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
13
17
|
|
|
14
18
|
export interface PlanValidationIssueInput {
|
|
@@ -46,30 +50,12 @@ function createIssue(params: {
|
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readPathValue(source: unknown, path: string): unknown {
|
|
54
|
-
if (!path.trim()) return source
|
|
55
|
-
|
|
56
|
-
let current: unknown = source
|
|
57
|
-
for (const segment of path
|
|
58
|
-
.split('.')
|
|
59
|
-
.map((part) => part.trim())
|
|
60
|
-
.filter(Boolean)) {
|
|
61
|
-
if (!isRecord(current)) return undefined
|
|
62
|
-
current = current[segment]
|
|
63
|
-
}
|
|
64
|
-
return current
|
|
65
|
-
}
|
|
66
|
-
|
|
67
53
|
function hasAllFields(value: unknown, fields: string[]): boolean {
|
|
68
54
|
if (!isRecord(value)) return false
|
|
69
55
|
return fields.every((field) => readPathValue(value, field) !== undefined)
|
|
70
56
|
}
|
|
71
57
|
|
|
72
|
-
function validateSchemaValue(params: { schema: PlanDataSchema; value: unknown; path: string }): string[] {
|
|
58
|
+
export function validateSchemaValue(params: { schema: PlanDataSchema; value: unknown; path: string }): string[] {
|
|
73
59
|
const issues: string[] = []
|
|
74
60
|
const { schema, value, path } = params
|
|
75
61
|
|
|
@@ -251,6 +237,24 @@ class PlanValidatorService {
|
|
|
251
237
|
}),
|
|
252
238
|
)
|
|
253
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
|
+
}
|
|
254
258
|
if (node.inputSchemaRef && !resolveSchemaRef(draft, node.inputSchemaRef)) {
|
|
255
259
|
blocking.push(
|
|
256
260
|
createIssue({
|
|
@@ -304,6 +308,61 @@ class PlanValidatorService {
|
|
|
304
308
|
}),
|
|
305
309
|
)
|
|
306
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
|
+
}
|
|
307
366
|
}
|
|
308
367
|
|
|
309
368
|
const entryNodeIds = draft.entryNodeIds ?? []
|
|
@@ -446,6 +505,14 @@ class PlanValidatorService {
|
|
|
446
505
|
)
|
|
447
506
|
}
|
|
448
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
|
+
|
|
449
516
|
return { blocking, warnings }
|
|
450
517
|
}
|
|
451
518
|
|