@lota-sdk/core 0.4.3 → 0.4.5
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/02_execution_plan.surql +4 -0
- package/package.json +2 -2
- package/src/ai-gateway/ai-gateway.ts +0 -11
- package/src/config/background-processing.ts +0 -9
- package/src/config/model-constants.ts +0 -1
- package/src/create-runtime.ts +0 -2
- package/src/db/service.ts +7 -25
- package/src/queues/autonomous-job.queue.ts +0 -4
- package/src/runtime/execution-plan.ts +2 -7
- package/src/services/agent-activity.service.ts +35 -5
- package/src/services/artifact.service.ts +12 -14
- package/src/services/autonomous-job.service.ts +8 -12
- package/src/services/execution-plan.service.ts +397 -41
- package/src/services/global-orchestrator.service.ts +1 -1
- package/src/services/ownership-dispatcher.service.ts +1 -1
- package/src/services/plan-agent-query.service.ts +4 -3
- package/src/services/plan-builder.service.ts +1 -6
- package/src/services/plan-deadline.service.ts +8 -9
- package/src/services/plan-run-data.ts +70 -4
- package/src/services/plan-run.service.ts +28 -1
- package/src/services/plan-scheduler.service.ts +4 -4
- package/src/services/plan-template.service.ts +6 -0
- package/src/tools/execution-plan.tool.ts +13 -2
- package/src/tools/index.ts +1 -0
- package/src/tools/plan-approval.tool.ts +66 -0
- package/src/workers/worker-utils.ts +0 -68
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
PlanDraft,
|
|
9
9
|
PlanNodeRunRecord,
|
|
10
10
|
PlanNodeSpecRecord,
|
|
11
|
+
PlanRunRecord,
|
|
11
12
|
PlanSpecRecord,
|
|
12
13
|
SerializableExecutionPlan,
|
|
13
14
|
SubmitPlanTurnResultArgs,
|
|
@@ -47,6 +48,25 @@ function aggregateBlockingIssues(issues: Array<{ code: string; message: string }
|
|
|
47
48
|
return issues.map((issue) => `${issue.code}: ${issue.message}`).join(' | ')
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function hasCrossThreadSourceContext(sourceThreadId: RecordIdInput | undefined, threadId: RecordIdInput): boolean {
|
|
52
|
+
if (!sourceThreadId) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return recordIdToString(sourceThreadId, TABLES.THREAD) !== recordIdToString(threadId, TABLES.THREAD)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isPlanVisibleInThreadContext(
|
|
60
|
+
threadId: RecordIdInput,
|
|
61
|
+
params: { threadId: RecordIdInput; sourceThreadId?: RecordIdInput },
|
|
62
|
+
): boolean {
|
|
63
|
+
const currentThreadId = recordIdToString(threadId, TABLES.THREAD)
|
|
64
|
+
return (
|
|
65
|
+
currentThreadId === recordIdToString(params.threadId, TABLES.THREAD) ||
|
|
66
|
+
(params.sourceThreadId !== undefined && currentThreadId === recordIdToString(params.sourceThreadId, TABLES.THREAD))
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
function toSpecData(spec: PlanSpecRecord, patch: Partial<PlanSpecRecord> & { replacedSpecId?: RecordIdInput | null }) {
|
|
51
71
|
return {
|
|
52
72
|
organizationId: ensureRecordId(spec.organizationId, TABLES.ORGANIZATION),
|
|
@@ -168,17 +188,29 @@ class ExecutionPlanService {
|
|
|
168
188
|
const runs = await planRunService.getActiveRunRecords(threadId)
|
|
169
189
|
if (runs.length === 0) return []
|
|
170
190
|
|
|
171
|
-
return await
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
return await this.serializeRuns(runs, options)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getPlansCreatedInContext(params: {
|
|
195
|
+
organizationId: RecordIdInput
|
|
196
|
+
sourceThreadId?: RecordIdInput
|
|
197
|
+
createdByAgentId?: string
|
|
198
|
+
statuses?: ReadonlyArray<PlanRunRecord['status']>
|
|
199
|
+
includeEvents?: boolean
|
|
200
|
+
includeArtifacts?: boolean
|
|
201
|
+
includeApprovals?: boolean
|
|
202
|
+
includeCheckpoints?: boolean
|
|
203
|
+
includeValidationIssues?: boolean
|
|
204
|
+
}): Promise<SerializableExecutionPlan[]> {
|
|
205
|
+
const runs = await planRunService.getRunsCreatedInContext({
|
|
206
|
+
organizationId: params.organizationId,
|
|
207
|
+
sourceThreadId: params.sourceThreadId,
|
|
208
|
+
createdByAgentId: params.createdByAgentId,
|
|
209
|
+
statuses: params.statuses,
|
|
210
|
+
})
|
|
211
|
+
if (runs.length === 0) return []
|
|
212
|
+
|
|
213
|
+
return await this.serializeRuns(runs, params)
|
|
182
214
|
}
|
|
183
215
|
|
|
184
216
|
async listActivePlanSummaries(threadId: RecordIdInput): Promise<ListExecutionPlansToolResultData> {
|
|
@@ -245,9 +277,14 @@ class ExecutionPlanService {
|
|
|
245
277
|
async createPlan(params: {
|
|
246
278
|
organizationId: RecordIdInput
|
|
247
279
|
threadId: RecordIdInput
|
|
280
|
+
sourceThreadId?: RecordIdInput
|
|
248
281
|
leadAgentId: string
|
|
282
|
+
createdByAgentId?: string
|
|
283
|
+
requireApproval?: boolean
|
|
249
284
|
input: PlanDraft
|
|
250
285
|
}): Promise<ExecutionPlanToolResultData> {
|
|
286
|
+
const requireApproval =
|
|
287
|
+
params.requireApproval ?? hasCrossThreadSourceContext(params.sourceThreadId, params.threadId)
|
|
251
288
|
const preparedDraft = planBuilderService.prepareDraft(PlanDraftSchema.parse(params.input))
|
|
252
289
|
const validation = planValidatorService.validateDraft(preparedDraft)
|
|
253
290
|
if (validation.blocking.length > 0) {
|
|
@@ -281,7 +318,10 @@ class ExecutionPlanService {
|
|
|
281
318
|
spec,
|
|
282
319
|
organizationId: params.organizationId,
|
|
283
320
|
threadId: params.threadId,
|
|
321
|
+
sourceThreadId: params.sourceThreadId,
|
|
284
322
|
leadAgentId: params.leadAgentId,
|
|
323
|
+
createdByAgentId: params.createdByAgentId,
|
|
324
|
+
requireApproval,
|
|
285
325
|
nodes: compiled.nodes,
|
|
286
326
|
emittedEvents,
|
|
287
327
|
createdEventType: 'plan-created',
|
|
@@ -293,25 +333,13 @@ class ExecutionPlanService {
|
|
|
293
333
|
|
|
294
334
|
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
295
335
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const schedule = await planSchedulerService.createSchedule({
|
|
336
|
+
if (!requireApproval) {
|
|
337
|
+
await this.attachPlanScheduleIfNeeded({
|
|
299
338
|
organizationId: params.organizationId,
|
|
300
339
|
threadId: params.threadId,
|
|
301
|
-
planSpecId: specId,
|
|
302
340
|
runId,
|
|
303
|
-
|
|
341
|
+
planSpecId: specId,
|
|
304
342
|
})
|
|
305
|
-
|
|
306
|
-
await databaseService.update(
|
|
307
|
-
TABLES.PLAN_RUN,
|
|
308
|
-
ensureRecordId(runId, TABLES.PLAN_RUN),
|
|
309
|
-
{
|
|
310
|
-
scheduleId: ensureRecordId(schedule.id, TABLES.PLAN_SCHEDULE),
|
|
311
|
-
scheduledAt: schedule.nextFireAt ? toDatabaseDateTime(schedule.nextFireAt) : undefined,
|
|
312
|
-
},
|
|
313
|
-
PlanRunSchema,
|
|
314
|
-
)
|
|
315
343
|
}
|
|
316
344
|
|
|
317
345
|
const plan = await this.finalizePlanSnapshot({ runId, emittedBy: params.leadAgentId })
|
|
@@ -323,7 +351,8 @@ class ExecutionPlanService {
|
|
|
323
351
|
threadId: RecordIdInput
|
|
324
352
|
organizationId: RecordIdInput
|
|
325
353
|
leadAgentId: string
|
|
326
|
-
|
|
354
|
+
createdByAgentId?: string
|
|
355
|
+
input: PlanDraft & { runId: string; reason: string; requireApproval?: boolean }
|
|
327
356
|
}): Promise<ExecutionPlanToolResultData> {
|
|
328
357
|
const activeRun = await planRunService.getRunById(params.input.runId)
|
|
329
358
|
const resolvedThreadId = activeRun.threadId
|
|
@@ -337,7 +366,7 @@ class ExecutionPlanService {
|
|
|
337
366
|
}
|
|
338
367
|
|
|
339
368
|
const activeSpec = await planRunService.getPlanSpecById(activeRun.planSpecId)
|
|
340
|
-
const { runId: _runId, reason: _reason, ...draftInput } = params.input
|
|
369
|
+
const { runId: _runId, reason: _reason, requireApproval: requestedRequireApproval, ...draftInput } = params.input
|
|
341
370
|
const preparedDraft = planBuilderService.prepareDraft(PlanDraftSchema.parse(draftInput))
|
|
342
371
|
const validation = planValidatorService.validateDraft(preparedDraft)
|
|
343
372
|
if (validation.blocking.length > 0) {
|
|
@@ -394,7 +423,11 @@ class ExecutionPlanService {
|
|
|
394
423
|
spec,
|
|
395
424
|
organizationId: params.organizationId,
|
|
396
425
|
threadId: resolvedThreadId,
|
|
426
|
+
sourceThreadId: activeRun.sourceThreadId,
|
|
397
427
|
leadAgentId: params.leadAgentId,
|
|
428
|
+
createdByAgentId: params.createdByAgentId ?? params.leadAgentId,
|
|
429
|
+
requireApproval:
|
|
430
|
+
requestedRequireApproval ?? hasCrossThreadSourceContext(activeRun.sourceThreadId, resolvedThreadId),
|
|
398
431
|
nodes: compiled.nodes,
|
|
399
432
|
emittedEvents,
|
|
400
433
|
runPatch: { replacedRunId: abortedRun.id },
|
|
@@ -410,6 +443,15 @@ class ExecutionPlanService {
|
|
|
410
443
|
|
|
411
444
|
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
412
445
|
|
|
446
|
+
if (!(requestedRequireApproval ?? hasCrossThreadSourceContext(activeRun.sourceThreadId, resolvedThreadId))) {
|
|
447
|
+
await this.attachPlanScheduleIfNeeded({
|
|
448
|
+
organizationId: params.organizationId,
|
|
449
|
+
threadId: resolvedThreadId,
|
|
450
|
+
runId,
|
|
451
|
+
planSpecId: specId,
|
|
452
|
+
})
|
|
453
|
+
}
|
|
454
|
+
|
|
413
455
|
const plan = await this.finalizePlanSnapshot({ runId, emittedBy: params.leadAgentId })
|
|
414
456
|
|
|
415
457
|
return buildExecutionPlanToolResult({
|
|
@@ -482,6 +524,243 @@ class ExecutionPlanService {
|
|
|
482
524
|
})
|
|
483
525
|
}
|
|
484
526
|
|
|
527
|
+
async approvePlan(params: {
|
|
528
|
+
organizationId: RecordIdInput
|
|
529
|
+
threadId: RecordIdInput
|
|
530
|
+
runId: RecordIdInput
|
|
531
|
+
emittedBy: string
|
|
532
|
+
}): Promise<SerializableExecutionPlan> {
|
|
533
|
+
const run = await planRunService.getRunById(params.runId)
|
|
534
|
+
if (
|
|
535
|
+
recordIdToString(run.organizationId, TABLES.ORGANIZATION) !==
|
|
536
|
+
recordIdToString(params.organizationId, TABLES.ORGANIZATION)
|
|
537
|
+
) {
|
|
538
|
+
throw new Error('Plan run belongs to a different organization.')
|
|
539
|
+
}
|
|
540
|
+
if (!isPlanVisibleInThreadContext(params.threadId, run)) {
|
|
541
|
+
throw new Error('Plan run is not available in this thread context.')
|
|
542
|
+
}
|
|
543
|
+
if (run.status !== 'pending-approval') {
|
|
544
|
+
throw new Error('Only pending-approval plans can be approved.')
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const [spec, nodeSpecs, nodeRuns, latestCheckpoint] = await Promise.all([
|
|
548
|
+
planRunService.getPlanSpecById(run.planSpecId),
|
|
549
|
+
planRunService.listNodeSpecs(run.planSpecId),
|
|
550
|
+
planRunService.listNodeRuns(run.id),
|
|
551
|
+
planRunService.getLatestCheckpoint(run.id),
|
|
552
|
+
])
|
|
553
|
+
const emittedEvents: PlanEventRecord[] = []
|
|
554
|
+
|
|
555
|
+
await databaseService.withTransaction(async (tx) => {
|
|
556
|
+
const activatedRun = PlanRunSchema.parse(
|
|
557
|
+
await tx
|
|
558
|
+
.update(ensureRecordId(run.id, TABLES.PLAN_RUN))
|
|
559
|
+
.content(toRunData(run, { status: 'running', startedAt: run.startedAt ?? new Date() }))
|
|
560
|
+
.output('after'),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
const synced = await planExecutorService.syncRunGraph({
|
|
564
|
+
tx,
|
|
565
|
+
run: activatedRun,
|
|
566
|
+
spec,
|
|
567
|
+
nodeSpecs,
|
|
568
|
+
nodeRuns,
|
|
569
|
+
artifacts: [],
|
|
570
|
+
emittedBy: params.emittedBy,
|
|
571
|
+
capturedEvents: emittedEvents,
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
const checkpoint = PlanCheckpointSchema.parse(
|
|
575
|
+
await tx
|
|
576
|
+
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
577
|
+
.content({
|
|
578
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
579
|
+
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
580
|
+
runStatus: synced.run.status,
|
|
581
|
+
readyNodeIds: [...synced.run.readyNodeIds],
|
|
582
|
+
activeNodeIds: synced.run.currentNodeId ? [synced.run.currentNodeId] : [],
|
|
583
|
+
artifactIds: [],
|
|
584
|
+
lastCompletedNodeIds: synced.nodeRuns
|
|
585
|
+
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
586
|
+
.map((nodeRun) => nodeRun.nodeId),
|
|
587
|
+
snapshot: {
|
|
588
|
+
reason: 'plan-approved',
|
|
589
|
+
currentNodeId: synced.run.currentNodeId,
|
|
590
|
+
waitingNodeId: synced.run.waitingNodeId,
|
|
591
|
+
readyNodeIds: synced.run.readyNodeIds,
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
.output('after'),
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
const updatedRun = PlanRunSchema.parse(
|
|
598
|
+
await tx
|
|
599
|
+
.update(ensureRecordId(synced.run.id, TABLES.PLAN_RUN))
|
|
600
|
+
.content(toRunData(synced.run, { lastCheckpointId: checkpoint.id }))
|
|
601
|
+
.output('after'),
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
const approvedEvent = await tx
|
|
605
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
606
|
+
.content({
|
|
607
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
608
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
609
|
+
eventType: 'plan-approved',
|
|
610
|
+
fromStatus: run.status,
|
|
611
|
+
toStatus: updatedRun.status,
|
|
612
|
+
message: `Approved execution plan "${spec.title}".`,
|
|
613
|
+
emittedBy: params.emittedBy,
|
|
614
|
+
detail: { title: spec.title, objective: spec.objective },
|
|
615
|
+
})
|
|
616
|
+
.output('after')
|
|
617
|
+
emittedEvents.push(PlanEventSchema.parse(approvedEvent))
|
|
618
|
+
|
|
619
|
+
const checkpointEvent = await tx
|
|
620
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
621
|
+
.content({
|
|
622
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
623
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
624
|
+
eventType: 'checkpoint-saved',
|
|
625
|
+
message: `Saved checkpoint ${(latestCheckpoint?.sequence ?? 0) + 1}.`,
|
|
626
|
+
emittedBy: 'system',
|
|
627
|
+
detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: 'plan-approved' },
|
|
628
|
+
})
|
|
629
|
+
.output('after')
|
|
630
|
+
emittedEvents.push(PlanEventSchema.parse(checkpointEvent))
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
634
|
+
await this.attachPlanScheduleIfNeeded({
|
|
635
|
+
organizationId: run.organizationId,
|
|
636
|
+
threadId: run.threadId,
|
|
637
|
+
runId: run.id,
|
|
638
|
+
planSpecId: run.planSpecId,
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
return await this.finalizePlanSnapshot({ runId: run.id, emittedBy: params.emittedBy })
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async rejectPlan(params: {
|
|
645
|
+
organizationId: RecordIdInput
|
|
646
|
+
threadId: RecordIdInput
|
|
647
|
+
runId: RecordIdInput
|
|
648
|
+
emittedBy: string
|
|
649
|
+
reason?: string
|
|
650
|
+
resolution?: 'rejected' | 'changes-requested'
|
|
651
|
+
}): Promise<SerializableExecutionPlan> {
|
|
652
|
+
const resolution = params.resolution ?? 'rejected'
|
|
653
|
+
const eventType = resolution === 'changes-requested' ? 'plan-changes-requested' : 'plan-rejected'
|
|
654
|
+
const run = await planRunService.getRunById(params.runId)
|
|
655
|
+
if (
|
|
656
|
+
recordIdToString(run.organizationId, TABLES.ORGANIZATION) !==
|
|
657
|
+
recordIdToString(params.organizationId, TABLES.ORGANIZATION)
|
|
658
|
+
) {
|
|
659
|
+
throw new Error('Plan run belongs to a different organization.')
|
|
660
|
+
}
|
|
661
|
+
if (!isPlanVisibleInThreadContext(params.threadId, run)) {
|
|
662
|
+
throw new Error('Plan run is not available in this thread context.')
|
|
663
|
+
}
|
|
664
|
+
if (run.status !== 'pending-approval') {
|
|
665
|
+
throw new Error('Only pending-approval plans can be rejected.')
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const [spec, nodeRuns, latestCheckpoint] = await Promise.all([
|
|
669
|
+
planRunService.getPlanSpecById(run.planSpecId),
|
|
670
|
+
planRunService.listNodeRuns(run.id),
|
|
671
|
+
planRunService.getLatestCheckpoint(run.id),
|
|
672
|
+
])
|
|
673
|
+
const emittedEvents: PlanEventRecord[] = []
|
|
674
|
+
const checkpointReason = resolution === 'changes-requested' ? 'plan-changes-requested' : 'plan-rejected'
|
|
675
|
+
|
|
676
|
+
await databaseService.withTransaction(async (tx) => {
|
|
677
|
+
const rejectedRun = PlanRunSchema.parse(
|
|
678
|
+
await tx
|
|
679
|
+
.update(ensureRecordId(run.id, TABLES.PLAN_RUN))
|
|
680
|
+
.content(
|
|
681
|
+
toRunData(run, {
|
|
682
|
+
status: 'aborted',
|
|
683
|
+
currentNodeId: null,
|
|
684
|
+
waitingNodeId: null,
|
|
685
|
+
readyNodeIds: [],
|
|
686
|
+
completedAt: new Date(),
|
|
687
|
+
}),
|
|
688
|
+
)
|
|
689
|
+
.output('after'),
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
const event = await tx
|
|
693
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
694
|
+
.content({
|
|
695
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
696
|
+
runId: ensureRecordId(rejectedRun.id, TABLES.PLAN_RUN),
|
|
697
|
+
eventType,
|
|
698
|
+
fromStatus: run.status,
|
|
699
|
+
toStatus: rejectedRun.status,
|
|
700
|
+
message:
|
|
701
|
+
resolution === 'changes-requested'
|
|
702
|
+
? `Requested changes for execution plan "${spec.title}".`
|
|
703
|
+
: `Rejected execution plan "${spec.title}".`,
|
|
704
|
+
emittedBy: params.emittedBy,
|
|
705
|
+
detail: { resolution, ...(params.reason ? { reason: params.reason } : {}) },
|
|
706
|
+
})
|
|
707
|
+
.output('after')
|
|
708
|
+
emittedEvents.push(PlanEventSchema.parse(event))
|
|
709
|
+
|
|
710
|
+
const checkpoint = PlanCheckpointSchema.parse(
|
|
711
|
+
await tx
|
|
712
|
+
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
713
|
+
.content({
|
|
714
|
+
runId: ensureRecordId(rejectedRun.id, TABLES.PLAN_RUN),
|
|
715
|
+
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
716
|
+
runStatus: rejectedRun.status,
|
|
717
|
+
readyNodeIds: [],
|
|
718
|
+
activeNodeIds: [],
|
|
719
|
+
artifactIds: [],
|
|
720
|
+
lastCompletedNodeIds: nodeRuns
|
|
721
|
+
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
722
|
+
.map((nodeRun) => nodeRun.nodeId),
|
|
723
|
+
snapshot: {
|
|
724
|
+
reason: checkpointReason,
|
|
725
|
+
resolution,
|
|
726
|
+
currentNodeId: null,
|
|
727
|
+
waitingNodeId: null,
|
|
728
|
+
readyNodeIds: [],
|
|
729
|
+
},
|
|
730
|
+
})
|
|
731
|
+
.output('after'),
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
const updatedRun = PlanRunSchema.parse(
|
|
735
|
+
await tx
|
|
736
|
+
.update(ensureRecordId(rejectedRun.id, TABLES.PLAN_RUN))
|
|
737
|
+
.content(toRunData(rejectedRun, { lastCheckpointId: checkpoint.id }))
|
|
738
|
+
.output('after'),
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
const checkpointEvent = await tx
|
|
742
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
743
|
+
.content({
|
|
744
|
+
planSpecId: ensureRecordId(spec.id, TABLES.PLAN_SPEC),
|
|
745
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
746
|
+
eventType: 'checkpoint-saved',
|
|
747
|
+
message: `Saved checkpoint ${(latestCheckpoint?.sequence ?? 0) + 1}.`,
|
|
748
|
+
emittedBy: 'system',
|
|
749
|
+
detail: {
|
|
750
|
+
checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT),
|
|
751
|
+
reason: checkpointReason,
|
|
752
|
+
resolution,
|
|
753
|
+
},
|
|
754
|
+
})
|
|
755
|
+
.output('after')
|
|
756
|
+
emittedEvents.push(PlanEventSchema.parse(checkpointEvent))
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
760
|
+
|
|
761
|
+
return await planRunService.toSerializablePlan(await planRunService.getRunById(run.id))
|
|
762
|
+
}
|
|
763
|
+
|
|
485
764
|
async applyApprovalResponseFromMessages(params: {
|
|
486
765
|
threadId: RecordIdInput
|
|
487
766
|
approvalMessages: ChatMessage[]
|
|
@@ -554,6 +833,23 @@ class ExecutionPlanService {
|
|
|
554
833
|
return ownershipDispatcherService.dispatchRunToStableBoundary({ runId: run.id, emittedBy: params.respondedBy })
|
|
555
834
|
}
|
|
556
835
|
|
|
836
|
+
private async serializeRuns(
|
|
837
|
+
runs: PlanRunRecord[],
|
|
838
|
+
options?: Partial<ExecutionPlanQueryArgs>,
|
|
839
|
+
): Promise<SerializableExecutionPlan[]> {
|
|
840
|
+
return await Promise.all(
|
|
841
|
+
runs.map((run) =>
|
|
842
|
+
planRunService.toSerializablePlan(run, {
|
|
843
|
+
includeEvents: options?.includeEvents,
|
|
844
|
+
includeArtifacts: options?.includeArtifacts,
|
|
845
|
+
includeApprovals: options?.includeApprovals,
|
|
846
|
+
includeCheckpoints: options?.includeCheckpoints,
|
|
847
|
+
includeValidationIssues: options?.includeValidationIssues,
|
|
848
|
+
}),
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
557
853
|
private async assertDispatchExecutors(preparedDraft: Parameters<typeof planValidatorService.validateDraft>[0]) {
|
|
558
854
|
const issues = ownershipDispatcherService.validateDraftExecutors(preparedDraft)
|
|
559
855
|
if (issues.length > 0) {
|
|
@@ -568,6 +864,41 @@ class ExecutionPlanService {
|
|
|
568
864
|
return ownershipDispatcherService.dispatchRunToStableBoundary({ runId: params.runId, emittedBy: params.emittedBy })
|
|
569
865
|
}
|
|
570
866
|
|
|
867
|
+
private async attachPlanScheduleIfNeeded(params: {
|
|
868
|
+
organizationId: RecordIdInput
|
|
869
|
+
threadId: RecordIdInput
|
|
870
|
+
runId: RecordIdInput
|
|
871
|
+
planSpecId: RecordIdInput
|
|
872
|
+
}): Promise<PlanRunRecord> {
|
|
873
|
+
const [run, spec] = await Promise.all([
|
|
874
|
+
planRunService.getRunById(params.runId),
|
|
875
|
+
planRunService.getPlanSpecById(params.planSpecId),
|
|
876
|
+
])
|
|
877
|
+
if (!spec.schedule || run.scheduleId) {
|
|
878
|
+
return run
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const schedule = await planSchedulerService.createSchedule({
|
|
882
|
+
organizationId: params.organizationId,
|
|
883
|
+
threadId: params.threadId,
|
|
884
|
+
planSpecId: params.planSpecId,
|
|
885
|
+
runId: params.runId,
|
|
886
|
+
scheduleSpec: spec.schedule,
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
const updatedRun = await databaseService.update(
|
|
890
|
+
TABLES.PLAN_RUN,
|
|
891
|
+
ensureRecordId(params.runId, TABLES.PLAN_RUN),
|
|
892
|
+
toRunData(run, {
|
|
893
|
+
scheduleId: schedule.id,
|
|
894
|
+
scheduledAt: schedule.nextFireAt ? toDatabaseDateTime(schedule.nextFireAt) : undefined,
|
|
895
|
+
}),
|
|
896
|
+
PlanRunSchema,
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
return updatedRun ?? run
|
|
900
|
+
}
|
|
901
|
+
|
|
571
902
|
private async createNodeSpecs(
|
|
572
903
|
tx: DatabaseTransaction,
|
|
573
904
|
planSpecId: RecordIdInput,
|
|
@@ -657,7 +988,10 @@ class ExecutionPlanService {
|
|
|
657
988
|
spec: PlanSpecRecord
|
|
658
989
|
organizationId: RecordIdInput
|
|
659
990
|
threadId: RecordIdInput
|
|
991
|
+
sourceThreadId?: RecordIdInput
|
|
660
992
|
leadAgentId: string
|
|
993
|
+
createdByAgentId?: string
|
|
994
|
+
requireApproval: boolean
|
|
661
995
|
nodes: CompiledPlanNode[]
|
|
662
996
|
emittedEvents: PlanEventRecord[]
|
|
663
997
|
createdEventType: 'plan-created' | 'plan-replaced'
|
|
@@ -665,7 +999,7 @@ class ExecutionPlanService {
|
|
|
665
999
|
createdEventDetail: Record<string, unknown>
|
|
666
1000
|
checkpointReason: 'plan-created' | 'plan-replaced'
|
|
667
1001
|
runPatch?: { replacedRunId?: RecordIdInput }
|
|
668
|
-
}): Promise<
|
|
1002
|
+
}): Promise<PlanRunRecord> {
|
|
669
1003
|
const nodeSpecs = await this.createNodeSpecs(params.tx, params.spec.id, params.nodes)
|
|
670
1004
|
const run = PlanRunSchema.parse(
|
|
671
1005
|
await params.tx
|
|
@@ -674,29 +1008,33 @@ class ExecutionPlanService {
|
|
|
674
1008
|
planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
|
|
675
1009
|
organizationId: ensureRecordId(params.organizationId, TABLES.ORGANIZATION),
|
|
676
1010
|
threadId: ensureRecordId(params.threadId, TABLES.THREAD),
|
|
1011
|
+
...(params.sourceThreadId ? { sourceThreadId: ensureRecordId(params.sourceThreadId, TABLES.THREAD) } : {}),
|
|
677
1012
|
leadAgentId: params.leadAgentId,
|
|
678
|
-
|
|
1013
|
+
...(params.createdByAgentId ? { createdByAgentId: params.createdByAgentId } : {}),
|
|
1014
|
+
status: params.requireApproval ? 'pending-approval' : 'running',
|
|
679
1015
|
readyNodeIds: [],
|
|
680
1016
|
failureCount: 0,
|
|
681
1017
|
...(params.runPatch?.replacedRunId
|
|
682
1018
|
? { replacedRunId: ensureRecordId(params.runPatch.replacedRunId, TABLES.PLAN_RUN) }
|
|
683
1019
|
: {}),
|
|
684
|
-
startedAt: new Date(),
|
|
1020
|
+
...(params.requireApproval ? {} : { startedAt: new Date() }),
|
|
685
1021
|
})
|
|
686
1022
|
.output('after'),
|
|
687
1023
|
)
|
|
688
1024
|
|
|
689
1025
|
const nodeRuns = await this.createNodeRuns(params.tx, run.id, params.spec.id, nodeSpecs)
|
|
690
|
-
const synced =
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1026
|
+
const synced = params.requireApproval
|
|
1027
|
+
? { run, nodeRuns }
|
|
1028
|
+
: await planExecutorService.syncRunGraph({
|
|
1029
|
+
tx: params.tx,
|
|
1030
|
+
run,
|
|
1031
|
+
spec: params.spec,
|
|
1032
|
+
nodeSpecs,
|
|
1033
|
+
nodeRuns,
|
|
1034
|
+
artifacts: [],
|
|
1035
|
+
emittedBy: params.leadAgentId,
|
|
1036
|
+
capturedEvents: params.emittedEvents,
|
|
1037
|
+
})
|
|
700
1038
|
|
|
701
1039
|
const event = await params.tx
|
|
702
1040
|
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
@@ -711,6 +1049,22 @@ class ExecutionPlanService {
|
|
|
711
1049
|
.output('after')
|
|
712
1050
|
params.emittedEvents.push(PlanEventSchema.parse(event))
|
|
713
1051
|
|
|
1052
|
+
if (params.requireApproval) {
|
|
1053
|
+
const pendingApprovalEvent = await params.tx
|
|
1054
|
+
.create(new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7()))
|
|
1055
|
+
.content({
|
|
1056
|
+
planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
|
|
1057
|
+
runId: ensureRecordId(synced.run.id, TABLES.PLAN_RUN),
|
|
1058
|
+
eventType: 'plan-pending-approval',
|
|
1059
|
+
toStatus: synced.run.status,
|
|
1060
|
+
message: `Execution plan "${params.spec.title}" is pending approval.`,
|
|
1061
|
+
emittedBy: params.leadAgentId,
|
|
1062
|
+
detail: { ...params.createdEventDetail, nodeCount: nodeSpecs.length },
|
|
1063
|
+
})
|
|
1064
|
+
.output('after')
|
|
1065
|
+
params.emittedEvents.push(PlanEventSchema.parse(pendingApprovalEvent))
|
|
1066
|
+
}
|
|
1067
|
+
|
|
714
1068
|
const checkpoint = PlanCheckpointSchema.parse(
|
|
715
1069
|
await params.tx
|
|
716
1070
|
.create(new RecordId(TABLES.PLAN_CHECKPOINT, Bun.randomUUIDv7()))
|
|
@@ -756,6 +1110,8 @@ class ExecutionPlanService {
|
|
|
756
1110
|
})
|
|
757
1111
|
.output('after')
|
|
758
1112
|
params.emittedEvents.push(PlanEventSchema.parse(checkpointEvent))
|
|
1113
|
+
|
|
1114
|
+
return updatedRun
|
|
759
1115
|
}
|
|
760
1116
|
}
|
|
761
1117
|
|
|
@@ -16,7 +16,7 @@ function formatDispatchError(error: unknown): string {
|
|
|
16
16
|
return error instanceof Error ? error.message : String(error)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const STABLE_RUN_STATUSES = new Set(['awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
|
|
19
|
+
const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
|
|
20
20
|
|
|
21
21
|
class GlobalOrchestratorService {
|
|
22
22
|
detectConvergence(params: {
|
|
@@ -34,7 +34,7 @@ import { systemExecutorService } from './system-executor.service'
|
|
|
34
34
|
import { ThreadSchema } from './thread.types'
|
|
35
35
|
import { userService } from './user.service'
|
|
36
36
|
|
|
37
|
-
const STABLE_RUN_STATUSES = new Set(['awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
|
|
37
|
+
const STABLE_RUN_STATUSES = new Set(['pending-approval', 'awaiting-human', 'blocked', 'failed', 'completed', 'aborted'])
|
|
38
38
|
const MAX_DISPATCH_ITERATIONS = 64
|
|
39
39
|
|
|
40
40
|
function toPlanNodeSpec(nodeSpec: PlanNodeSpecRecord): PlanNodeSpec {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PlanExecutionVisibility, PlanNodeSpecRecord, PlanRunRecord, PlanSpecRecord } from '@lota-sdk/shared'
|
|
2
2
|
import { PlanRunSchema } from '@lota-sdk/shared'
|
|
3
|
+
import { BoundQuery } from 'surrealdb'
|
|
3
4
|
|
|
4
5
|
import type { RecordIdInput } from '../db/record-id'
|
|
5
6
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
@@ -254,10 +255,10 @@ class PlanAgentQueryService {
|
|
|
254
255
|
|
|
255
256
|
const whereOrganization = organizationId ? ' AND organizationId = $organizationId' : ''
|
|
256
257
|
return databaseService.queryMany(
|
|
257
|
-
|
|
258
|
-
|
|
258
|
+
new BoundQuery(
|
|
259
|
+
`SELECT * FROM ${TABLES.PLAN_RUN} WHERE status INSIDE $statuses${whereOrganization} ORDER BY updatedAt DESC`,
|
|
259
260
|
bindings,
|
|
260
|
-
|
|
261
|
+
),
|
|
261
262
|
PlanRunSchema,
|
|
262
263
|
)
|
|
263
264
|
}
|
|
@@ -18,10 +18,6 @@ function buildImplicitLinearEdges(draft: PlanDraft) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class PlanBuilderService {
|
|
21
|
-
roleAssignment(draft: PlanDraft): PlanDraft {
|
|
22
|
-
return draft
|
|
23
|
-
}
|
|
24
|
-
|
|
25
21
|
structureDesign(draft: PlanDraft): PlanDraft {
|
|
26
22
|
return {
|
|
27
23
|
...draft,
|
|
@@ -63,8 +59,7 @@ class PlanBuilderService {
|
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
prepareDraft(draft: PlanDraft): PlanDraft {
|
|
66
|
-
const
|
|
67
|
-
const withStructure = this.structureDesign(withRoles)
|
|
62
|
+
const withStructure = this.structureDesign(draft)
|
|
68
63
|
return this.semanticCompletion(withStructure)
|
|
69
64
|
}
|
|
70
65
|
}
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
PlanRunRecord,
|
|
9
9
|
} from '@lota-sdk/shared'
|
|
10
10
|
import { PlanEventSchema, PlanNodeRunSchema, PlanNodeSpecRecordSchema, PlanRunSchema } from '@lota-sdk/shared'
|
|
11
|
-
import { RecordId } from 'surrealdb'
|
|
11
|
+
import { BoundQuery, RecordId } from 'surrealdb'
|
|
12
12
|
|
|
13
13
|
import type { RecordIdInput } from '../db/record-id'
|
|
14
14
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
@@ -185,10 +185,9 @@ class PlanDeadlineService {
|
|
|
185
185
|
entries: Array<{ nodeRun: PlanNodeRunRecord; nodeSpec: PlanNodeSpecRecord; evaluation: DeadlineEvaluationResult }>
|
|
186
186
|
}> {
|
|
187
187
|
const activeNodeRuns = await databaseService.queryMany(
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
},
|
|
188
|
+
new BoundQuery(`SELECT * FROM ${TABLES.PLAN_NODE_RUN} WHERE status IN $statuses`, {
|
|
189
|
+
statuses: ['running', 'awaiting-human'],
|
|
190
|
+
}),
|
|
192
191
|
PlanNodeRunSchema,
|
|
193
192
|
)
|
|
194
193
|
|
|
@@ -380,20 +379,20 @@ class PlanDeadlineService {
|
|
|
380
379
|
|
|
381
380
|
if (dedupeKey) {
|
|
382
381
|
const existing = await databaseService.queryMany(
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
new BoundQuery(
|
|
383
|
+
`SELECT * FROM ${TABLES.PLAN_EVENT}
|
|
385
384
|
WHERE runId = $runId
|
|
386
385
|
AND nodeId = $nodeId
|
|
387
386
|
AND eventType = $eventType
|
|
388
387
|
AND detail.dedupeKey = $dedupeKey
|
|
389
388
|
LIMIT 1`,
|
|
390
|
-
|
|
389
|
+
{
|
|
391
390
|
runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
|
|
392
391
|
nodeId: params.nodeRun.nodeId,
|
|
393
392
|
eventType: params.eventType,
|
|
394
393
|
dedupeKey,
|
|
395
394
|
},
|
|
396
|
-
|
|
395
|
+
),
|
|
397
396
|
PlanEventSchema,
|
|
398
397
|
)
|
|
399
398
|
if (existing.length > 0) {
|