@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
|
@@ -21,23 +21,30 @@ import {
|
|
|
21
21
|
} from '@lota-sdk/shared'
|
|
22
22
|
import { RecordId } from 'surrealdb'
|
|
23
23
|
|
|
24
|
+
import { aiLogger } from '../config/logger'
|
|
24
25
|
import type { RecordIdInput } from '../db/record-id'
|
|
25
26
|
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
26
27
|
import { databaseService } from '../db/service'
|
|
27
28
|
import type { DatabaseTransaction } from '../db/service'
|
|
28
29
|
import { TABLES } from '../db/tables'
|
|
30
|
+
import { toDatabaseDateTime } from '../utils/date-time'
|
|
29
31
|
import { isRecord } from '../utils/string'
|
|
32
|
+
import { feedbackLoopService } from './feedback-loop.service'
|
|
33
|
+
import { institutionalMemoryService } from './institutional-memory.service'
|
|
30
34
|
import { planApprovalService } from './plan-approval.service'
|
|
31
35
|
import { planArtifactService } from './plan-artifact.service'
|
|
32
36
|
import { planCheckpointService } from './plan-checkpoint.service'
|
|
37
|
+
import { planCoordinationService } from './plan-coordination.service'
|
|
33
38
|
import { readPathValue } from './plan-helpers'
|
|
34
39
|
import { planRunService } from './plan-run.service'
|
|
40
|
+
import { planSchedulerService } from './plan-scheduler.service'
|
|
35
41
|
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
36
42
|
import { planValidatorService } from './plan-validator.service'
|
|
43
|
+
import { qualityMetricsService } from './quality-metrics.service'
|
|
37
44
|
|
|
38
|
-
const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped'])
|
|
45
|
+
const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped', 'scheduled', 'monitoring'])
|
|
39
46
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
40
|
-
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
47
|
+
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
|
|
41
48
|
|
|
42
49
|
function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
|
|
43
50
|
const segments = path
|
|
@@ -146,8 +153,8 @@ type PlanRunUpdate = Omit<
|
|
|
146
153
|
waitingNodeId?: string | null
|
|
147
154
|
replacedRunId?: RecordIdInput | null
|
|
148
155
|
lastCheckpointId?: RecordIdInput | null
|
|
149
|
-
startedAt?: Date | null
|
|
150
|
-
completedAt?: Date | null
|
|
156
|
+
startedAt?: string | Date | null
|
|
157
|
+
completedAt?: string | Date | null
|
|
151
158
|
}
|
|
152
159
|
|
|
153
160
|
type PlanNodeRunUpdate = Omit<
|
|
@@ -158,6 +165,7 @@ type PlanNodeRunUpdate = Omit<
|
|
|
158
165
|
| 'latestStructuredOutput'
|
|
159
166
|
| 'latestNotes'
|
|
160
167
|
| 'latestAttemptId'
|
|
168
|
+
| 'scheduledAt'
|
|
161
169
|
| 'readyAt'
|
|
162
170
|
| 'startedAt'
|
|
163
171
|
| 'completedAt'
|
|
@@ -168,9 +176,10 @@ type PlanNodeRunUpdate = Omit<
|
|
|
168
176
|
latestStructuredOutput?: Record<string, unknown> | null
|
|
169
177
|
latestNotes?: string | null
|
|
170
178
|
latestAttemptId?: RecordIdInput | null
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
scheduledAt?: string | Date | null
|
|
180
|
+
readyAt?: string | Date | null
|
|
181
|
+
startedAt?: string | Date | null
|
|
182
|
+
completedAt?: string | Date | null
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
@@ -213,16 +222,16 @@ function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
|
213
222
|
...(patch.startedAt === null
|
|
214
223
|
? {}
|
|
215
224
|
: patch.startedAt !== undefined
|
|
216
|
-
? { startedAt: patch.startedAt }
|
|
225
|
+
? { startedAt: toDatabaseDateTime(patch.startedAt) }
|
|
217
226
|
: run.startedAt
|
|
218
|
-
? { startedAt: run.startedAt }
|
|
227
|
+
? { startedAt: toDatabaseDateTime(run.startedAt) }
|
|
219
228
|
: {}),
|
|
220
229
|
...(patch.completedAt === null
|
|
221
230
|
? {}
|
|
222
231
|
: patch.completedAt !== undefined
|
|
223
|
-
? { completedAt: patch.completedAt }
|
|
232
|
+
? { completedAt: toDatabaseDateTime(patch.completedAt) }
|
|
224
233
|
: run.completedAt
|
|
225
|
-
? { completedAt: run.completedAt }
|
|
234
|
+
? { completedAt: toDatabaseDateTime(run.completedAt) }
|
|
226
235
|
: {}),
|
|
227
236
|
}
|
|
228
237
|
}
|
|
@@ -277,26 +286,33 @@ function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
|
|
|
277
286
|
: nodeRun.failureClass
|
|
278
287
|
? { failureClass: nodeRun.failureClass }
|
|
279
288
|
: {}),
|
|
289
|
+
...(patch.scheduledAt === null
|
|
290
|
+
? {}
|
|
291
|
+
: patch.scheduledAt !== undefined
|
|
292
|
+
? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
|
|
293
|
+
: nodeRun.scheduledAt
|
|
294
|
+
? { scheduledAt: toDatabaseDateTime(nodeRun.scheduledAt) }
|
|
295
|
+
: {}),
|
|
280
296
|
...(patch.readyAt === null
|
|
281
297
|
? {}
|
|
282
298
|
: patch.readyAt !== undefined
|
|
283
|
-
? { readyAt: patch.readyAt }
|
|
299
|
+
? { readyAt: toDatabaseDateTime(patch.readyAt) }
|
|
284
300
|
: nodeRun.readyAt
|
|
285
|
-
? { readyAt: nodeRun.readyAt }
|
|
301
|
+
? { readyAt: toDatabaseDateTime(nodeRun.readyAt) }
|
|
286
302
|
: {}),
|
|
287
303
|
...(patch.startedAt === null
|
|
288
304
|
? {}
|
|
289
305
|
: patch.startedAt !== undefined
|
|
290
|
-
? { startedAt: patch.startedAt }
|
|
306
|
+
? { startedAt: toDatabaseDateTime(patch.startedAt) }
|
|
291
307
|
: nodeRun.startedAt
|
|
292
|
-
? { startedAt: nodeRun.startedAt }
|
|
308
|
+
? { startedAt: toDatabaseDateTime(nodeRun.startedAt) }
|
|
293
309
|
: {}),
|
|
294
310
|
...(patch.completedAt === null
|
|
295
311
|
? {}
|
|
296
312
|
: patch.completedAt !== undefined
|
|
297
|
-
? { completedAt: patch.completedAt }
|
|
313
|
+
? { completedAt: toDatabaseDateTime(patch.completedAt) }
|
|
298
314
|
: nodeRun.completedAt
|
|
299
|
-
? { completedAt: nodeRun.completedAt }
|
|
315
|
+
? { completedAt: toDatabaseDateTime(nodeRun.completedAt) }
|
|
300
316
|
: {}),
|
|
301
317
|
}
|
|
302
318
|
}
|
|
@@ -751,7 +767,63 @@ class PlanExecutorService {
|
|
|
751
767
|
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
752
768
|
})
|
|
753
769
|
|
|
754
|
-
|
|
770
|
+
// Record node-level quality metrics (fire-and-forget)
|
|
771
|
+
const orgId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
|
|
772
|
+
const runIdStr = recordIdToString(run.id, TABLES.PLAN_RUN)
|
|
773
|
+
const nodeStartedAt = nodeRun.startedAt
|
|
774
|
+
const executionTimeMs = nodeStartedAt ? Date.now() - new Date(nodeStartedAt).getTime() : 0
|
|
775
|
+
qualityMetricsService
|
|
776
|
+
.recordNodeMetrics({
|
|
777
|
+
organizationId: orgId,
|
|
778
|
+
runId: runIdStr,
|
|
779
|
+
nodeId: params.nodeId,
|
|
780
|
+
metrics: {
|
|
781
|
+
executionTimeMs: Math.max(0, executionTimeMs),
|
|
782
|
+
attemptCount: nodeRun.attemptCount + 1,
|
|
783
|
+
artifactCount: params.result.artifacts.length,
|
|
784
|
+
validationIssueCount: validation.blocking.length + validation.warnings.length,
|
|
785
|
+
ownerRef: nodeSpec.owner.ref,
|
|
786
|
+
ownerType: nodeSpec.owner.executorType,
|
|
787
|
+
nodeType: nodeSpec.type,
|
|
788
|
+
},
|
|
789
|
+
})
|
|
790
|
+
.catch((error) => {
|
|
791
|
+
aiLogger.warn`Failed to record node quality metrics for run ${runIdStr} node ${params.nodeId}: ${error instanceof Error ? error.message : String(error)}`
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
// If the run just completed, record cycle metrics, persist feedback recommendations, and extract patterns
|
|
795
|
+
const updatedRun = await planRunService.getRunById(run.id)
|
|
796
|
+
if (updatedRun.status === 'completed') {
|
|
797
|
+
qualityMetricsService.recordCycleMetrics({ organizationId: orgId, runId: runIdStr }).catch((error) => {
|
|
798
|
+
aiLogger.warn`Failed to record cycle quality metrics for run ${runIdStr}: ${error instanceof Error ? error.message : String(error)}`
|
|
799
|
+
})
|
|
800
|
+
feedbackLoopService
|
|
801
|
+
.analyzeOutcomes({ runId: runIdStr, organizationId: orgId })
|
|
802
|
+
.then(async (recommendations) => {
|
|
803
|
+
if (recommendations.length === 0) return
|
|
804
|
+
const specRecord = await planRunService.getPlanSpecById(updatedRun.planSpecId)
|
|
805
|
+
await databaseService.create(
|
|
806
|
+
TABLES.PLAN_EVENT,
|
|
807
|
+
{
|
|
808
|
+
planSpecId: ensureRecordId(specRecord.id, TABLES.PLAN_SPEC),
|
|
809
|
+
runId: ensureRecordId(updatedRun.id, TABLES.PLAN_RUN),
|
|
810
|
+
eventType: 'feedback-analyzed',
|
|
811
|
+
message: `Feedback analysis produced ${recommendations.length} recommendation(s).`,
|
|
812
|
+
detail: { recommendations },
|
|
813
|
+
emittedBy: 'system',
|
|
814
|
+
},
|
|
815
|
+
PlanEventSchema,
|
|
816
|
+
)
|
|
817
|
+
})
|
|
818
|
+
.catch((error) => {
|
|
819
|
+
aiLogger.warn`Failed to analyze feedback outcomes for run ${runIdStr}: ${error instanceof Error ? error.message : String(error)}`
|
|
820
|
+
})
|
|
821
|
+
institutionalMemoryService.extractPatterns({ organizationId: orgId, runId: runIdStr }).catch((error) => {
|
|
822
|
+
aiLogger.warn`Failed to extract institutional memory patterns for run ${runIdStr}: ${error instanceof Error ? error.message : String(error)}`
|
|
823
|
+
})
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const snapshot = await planRunService.toSerializablePlan(updatedRun, {
|
|
755
827
|
includeEvents: true,
|
|
756
828
|
includeArtifacts: true,
|
|
757
829
|
includeApprovals: true,
|
|
@@ -992,7 +1064,7 @@ class PlanExecutorService {
|
|
|
992
1064
|
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
993
1065
|
})
|
|
994
1066
|
|
|
995
|
-
return
|
|
1067
|
+
return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
|
|
996
1068
|
includeEvents: true,
|
|
997
1069
|
includeArtifacts: true,
|
|
998
1070
|
includeApprovals: true,
|
|
@@ -1099,6 +1171,176 @@ class PlanExecutorService {
|
|
|
1099
1171
|
})
|
|
1100
1172
|
}
|
|
1101
1173
|
|
|
1174
|
+
async transitionNodeToRunning(params: { runId: string; nodeId: string }): Promise<void> {
|
|
1175
|
+
const run = await planRunService.getRunById(params.runId)
|
|
1176
|
+
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
1177
|
+
if (nodeRun.status !== 'ready') return
|
|
1178
|
+
|
|
1179
|
+
await databaseService.withTransaction(async (tx) => {
|
|
1180
|
+
const runningNodeRun = PlanNodeRunSchema.parse(
|
|
1181
|
+
await tx
|
|
1182
|
+
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1183
|
+
.content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? new Date() }))
|
|
1184
|
+
.output('after'),
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
1188
|
+
await this.replaceRun(tx, run, {
|
|
1189
|
+
status: 'running',
|
|
1190
|
+
currentNodeId: runningNodeRun.nodeId,
|
|
1191
|
+
waitingNodeId: null,
|
|
1192
|
+
readyNodeIds: nodeRuns
|
|
1193
|
+
.filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
|
|
1194
|
+
.map((candidate) => candidate.nodeId),
|
|
1195
|
+
})
|
|
1196
|
+
})
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async blockNodeOnDispatchFailure(params: {
|
|
1200
|
+
workstreamId: RecordIdInput
|
|
1201
|
+
runId: string
|
|
1202
|
+
nodeId: string
|
|
1203
|
+
emittedBy: string
|
|
1204
|
+
message: string
|
|
1205
|
+
failureClass: PlanFailureClass
|
|
1206
|
+
}): Promise<SerializableExecutionPlan> {
|
|
1207
|
+
const run = await planRunService.getRunById(params.runId)
|
|
1208
|
+
if (
|
|
1209
|
+
recordIdToString(run.workstreamId, TABLES.WORKSTREAM) !== recordIdToString(params.workstreamId, TABLES.WORKSTREAM)
|
|
1210
|
+
) {
|
|
1211
|
+
throw new Error('Execution run belongs to a different workstream.')
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
1215
|
+
const nodeSpec = await planRunService.getNodeSpecByNodeId(spec.id, params.nodeId)
|
|
1216
|
+
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
1217
|
+
const artifacts = await planRunService.listArtifacts(run.id)
|
|
1218
|
+
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
1219
|
+
|
|
1220
|
+
await databaseService.withTransaction(async (tx) => {
|
|
1221
|
+
const blockedNodeRun =
|
|
1222
|
+
nodeRun.status === 'blocked'
|
|
1223
|
+
? nodeRun
|
|
1224
|
+
: PlanNodeRunSchema.parse(
|
|
1225
|
+
await tx
|
|
1226
|
+
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1227
|
+
.content(
|
|
1228
|
+
toNodeRunData(nodeRun, {
|
|
1229
|
+
status: 'blocked',
|
|
1230
|
+
blockedReason: params.message,
|
|
1231
|
+
failureClass: params.failureClass,
|
|
1232
|
+
}),
|
|
1233
|
+
)
|
|
1234
|
+
.output('after'),
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
const blockedRun = await this.replaceRun(tx, run, {
|
|
1238
|
+
status: 'blocked',
|
|
1239
|
+
currentNodeId: blockedNodeRun.nodeId,
|
|
1240
|
+
waitingNodeId: null,
|
|
1241
|
+
readyNodeIds: [],
|
|
1242
|
+
failureCount: run.failureCount + 1,
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
await this.emitEvent({
|
|
1246
|
+
tx,
|
|
1247
|
+
run: blockedRun,
|
|
1248
|
+
spec,
|
|
1249
|
+
nodeId: blockedNodeRun.nodeId,
|
|
1250
|
+
eventType: 'node-blocked',
|
|
1251
|
+
fromStatus: nodeRun.status,
|
|
1252
|
+
toStatus: blockedNodeRun.status,
|
|
1253
|
+
message: `Node "${nodeSpec.label}" failed during owner dispatch.`,
|
|
1254
|
+
detail: { failureClass: params.failureClass, phase: 'dispatch', error: params.message },
|
|
1255
|
+
emittedBy: params.emittedBy,
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
const checkpoint = await this.saveCheckpoint({
|
|
1259
|
+
tx,
|
|
1260
|
+
run: blockedRun,
|
|
1261
|
+
nodeRuns: (await planRunService.listNodeRuns(run.id)).map((candidate) =>
|
|
1262
|
+
candidate.nodeId === blockedNodeRun.nodeId ? blockedNodeRun : candidate,
|
|
1263
|
+
),
|
|
1264
|
+
artifacts,
|
|
1265
|
+
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
1266
|
+
reason: 'owner-dispatch-failed',
|
|
1267
|
+
})
|
|
1268
|
+
await this.attachCheckpoint(tx, blockedRun, checkpoint)
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
|
|
1272
|
+
includeEvents: true,
|
|
1273
|
+
includeArtifacts: true,
|
|
1274
|
+
includeApprovals: true,
|
|
1275
|
+
includeCheckpoints: true,
|
|
1276
|
+
includeValidationIssues: true,
|
|
1277
|
+
})
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async promoteDelayedNode(params: { runId: string; nodeId: string; emittedBy: string }): Promise<void> {
|
|
1281
|
+
const run = await planRunService.getRunById(params.runId)
|
|
1282
|
+
if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
|
|
1283
|
+
return // Run is no longer active, skip promotion
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
1287
|
+
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
1288
|
+
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
1289
|
+
|
|
1290
|
+
// Only promote if still in scheduled state (delay hasn't been superseded)
|
|
1291
|
+
if (nodeRun.status !== 'scheduled') return
|
|
1292
|
+
|
|
1293
|
+
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
1294
|
+
const artifacts = await planRunService.listArtifacts(run.id)
|
|
1295
|
+
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
1296
|
+
|
|
1297
|
+
await databaseService.withTransaction(async (tx) => {
|
|
1298
|
+
const readyNodeRun = PlanNodeRunSchema.parse(
|
|
1299
|
+
await tx
|
|
1300
|
+
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1301
|
+
.content(toNodeRunData(nodeRun, { status: 'ready', readyAt: new Date() }))
|
|
1302
|
+
.output('after'),
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1305
|
+
const updatedNodeRuns = nodeRuns.map((candidate) =>
|
|
1306
|
+
candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
const nodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
|
|
1310
|
+
await this.emitEvent({
|
|
1311
|
+
tx,
|
|
1312
|
+
run,
|
|
1313
|
+
spec,
|
|
1314
|
+
nodeId: readyNodeRun.nodeId,
|
|
1315
|
+
eventType: 'node-ready',
|
|
1316
|
+
fromStatus: nodeRun.status,
|
|
1317
|
+
toStatus: readyNodeRun.status,
|
|
1318
|
+
message: `Node "${nodeSpec?.label ?? params.nodeId}" promoted to ready after delay.`,
|
|
1319
|
+
emittedBy: params.emittedBy,
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
const synced = await this.syncRunGraph({
|
|
1323
|
+
tx,
|
|
1324
|
+
run,
|
|
1325
|
+
spec,
|
|
1326
|
+
nodeSpecs,
|
|
1327
|
+
nodeRuns: updatedNodeRuns,
|
|
1328
|
+
artifacts,
|
|
1329
|
+
emittedBy: params.emittedBy,
|
|
1330
|
+
})
|
|
1331
|
+
|
|
1332
|
+
const checkpoint = await this.saveCheckpoint({
|
|
1333
|
+
tx,
|
|
1334
|
+
run: synced.run,
|
|
1335
|
+
nodeRuns: synced.nodeRuns,
|
|
1336
|
+
artifacts: synced.artifacts,
|
|
1337
|
+
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
1338
|
+
reason: 'delayed-node-promoted',
|
|
1339
|
+
})
|
|
1340
|
+
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
1341
|
+
})
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1102
1344
|
async syncRunGraph(params: {
|
|
1103
1345
|
tx: DatabaseTransaction
|
|
1104
1346
|
run: PlanRunRecord
|
|
@@ -1133,6 +1375,28 @@ class PlanExecutorService {
|
|
|
1133
1375
|
const currentArtifacts = [...params.artifacts]
|
|
1134
1376
|
const sortedNodeSpecs = [...params.nodeSpecs].sort((left, right) => left.position - right.position)
|
|
1135
1377
|
|
|
1378
|
+
// Cross-plan dependency check: if spec has block-mode dependencies that are unresolved, block the run
|
|
1379
|
+
if (params.spec.dependencies && params.spec.dependencies.length > 0) {
|
|
1380
|
+
const { unresolved } = await planCoordinationService.resolveDependencies({
|
|
1381
|
+
dependencies: params.spec.dependencies,
|
|
1382
|
+
workstreamId: recordIdToString(params.spec.workstreamId, TABLES.WORKSTREAM),
|
|
1383
|
+
})
|
|
1384
|
+
if (unresolved.length > 0) {
|
|
1385
|
+
currentRun = await this.replaceRun(params.tx, currentRun, { status: 'blocked', readyNodeIds: [] })
|
|
1386
|
+
await this.emitEvent({
|
|
1387
|
+
tx: params.tx,
|
|
1388
|
+
run: currentRun,
|
|
1389
|
+
spec: params.spec,
|
|
1390
|
+
eventType: 'run-status-changed',
|
|
1391
|
+
fromStatus: params.run.status,
|
|
1392
|
+
toStatus: currentRun.status,
|
|
1393
|
+
message: `Run blocked: unresolved cross-plan dependencies (${unresolved.map((d) => d.sourcePlanTitle).join(', ')}).`,
|
|
1394
|
+
emittedBy: params.emittedBy,
|
|
1395
|
+
})
|
|
1396
|
+
return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1136
1400
|
const replaceNodeRun = (nextNodeRun: PlanNodeRunRecord) => {
|
|
1137
1401
|
currentNodeRuns = currentNodeRuns.map((candidate) =>
|
|
1138
1402
|
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
@@ -1207,25 +1471,89 @@ class PlanExecutorService {
|
|
|
1207
1471
|
}
|
|
1208
1472
|
|
|
1209
1473
|
const resolvedInput = this.buildResolvedInput({ spec: params.spec, nodeSpec, nodeRunsById, artifactsByNodeId })
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1474
|
+
|
|
1475
|
+
const nodeSchedule = nodeSpec.schedule
|
|
1476
|
+
const hasNonImmediateSchedule = nodeSchedule && nodeSchedule.type !== 'immediate'
|
|
1477
|
+
|
|
1478
|
+
if (hasNonImmediateSchedule) {
|
|
1479
|
+
const scheduledNodeRun = PlanNodeRunSchema.parse(
|
|
1480
|
+
await params.tx
|
|
1481
|
+
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1482
|
+
.content(toNodeRunData(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: new Date() }))
|
|
1483
|
+
.output('after'),
|
|
1484
|
+
)
|
|
1485
|
+
replaceNodeRun(scheduledNodeRun)
|
|
1486
|
+
await planSchedulerService.createSchedule({
|
|
1487
|
+
organizationId: currentRun.organizationId,
|
|
1488
|
+
workstreamId: currentRun.workstreamId,
|
|
1489
|
+
planSpecId: params.spec.id,
|
|
1490
|
+
runId: currentRun.id,
|
|
1491
|
+
nodeId: nodeSpec.nodeId,
|
|
1492
|
+
scheduleSpec: nodeSchedule,
|
|
1493
|
+
})
|
|
1494
|
+
await this.emitEvent({
|
|
1495
|
+
tx: params.tx,
|
|
1496
|
+
run: currentRun,
|
|
1497
|
+
spec: params.spec,
|
|
1498
|
+
nodeId: scheduledNodeRun.nodeId,
|
|
1499
|
+
eventType: 'node-scheduled',
|
|
1500
|
+
fromStatus: nodeRun.status,
|
|
1501
|
+
toStatus: scheduledNodeRun.status,
|
|
1502
|
+
message: `Node "${nodeSpec.label}" is scheduled (${nodeSchedule.type}).`,
|
|
1503
|
+
emittedBy: params.emittedBy,
|
|
1504
|
+
})
|
|
1505
|
+
changed = true
|
|
1506
|
+
} else if (nodeSpec.delayAfterPredecessorMs) {
|
|
1507
|
+
// Event-triggered delay: enqueue a delayed promotion instead of transitioning to ready immediately
|
|
1508
|
+
const { enqueueDelayedNodePromotion } = await import('../queues/delayed-node-promotion.queue')
|
|
1509
|
+
const scheduledNodeRun = PlanNodeRunSchema.parse(
|
|
1510
|
+
await params.tx
|
|
1511
|
+
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1512
|
+
.content(toNodeRunData(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: new Date() }))
|
|
1513
|
+
.output('after'),
|
|
1514
|
+
)
|
|
1515
|
+
replaceNodeRun(scheduledNodeRun)
|
|
1516
|
+
await enqueueDelayedNodePromotion(
|
|
1517
|
+
{
|
|
1518
|
+
runId: recordIdToString(currentRun.id, TABLES.PLAN_RUN),
|
|
1519
|
+
nodeId: nodeSpec.nodeId,
|
|
1520
|
+
emittedBy: params.emittedBy,
|
|
1521
|
+
},
|
|
1522
|
+
nodeSpec.delayAfterPredecessorMs,
|
|
1523
|
+
)
|
|
1524
|
+
await this.emitEvent({
|
|
1525
|
+
tx: params.tx,
|
|
1526
|
+
run: currentRun,
|
|
1527
|
+
spec: params.spec,
|
|
1528
|
+
nodeId: scheduledNodeRun.nodeId,
|
|
1529
|
+
eventType: 'node-scheduled',
|
|
1530
|
+
fromStatus: nodeRun.status,
|
|
1531
|
+
toStatus: scheduledNodeRun.status,
|
|
1532
|
+
message: `Node "${nodeSpec.label}" is delayed by ${nodeSpec.delayAfterPredecessorMs}ms after predecessor.`,
|
|
1533
|
+
emittedBy: params.emittedBy,
|
|
1534
|
+
})
|
|
1535
|
+
changed = true
|
|
1536
|
+
} else {
|
|
1537
|
+
const readyNodeRun = PlanNodeRunSchema.parse(
|
|
1538
|
+
await params.tx
|
|
1539
|
+
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1540
|
+
.content(toNodeRunData(nodeRun, { status: 'ready', resolvedInput, readyAt: new Date() }))
|
|
1541
|
+
.output('after'),
|
|
1542
|
+
)
|
|
1543
|
+
replaceNodeRun(readyNodeRun)
|
|
1544
|
+
await this.emitEvent({
|
|
1545
|
+
tx: params.tx,
|
|
1546
|
+
run: currentRun,
|
|
1547
|
+
spec: params.spec,
|
|
1548
|
+
nodeId: readyNodeRun.nodeId,
|
|
1549
|
+
eventType: 'node-ready',
|
|
1550
|
+
fromStatus: nodeRun.status,
|
|
1551
|
+
toStatus: readyNodeRun.status,
|
|
1552
|
+
message: `Node "${nodeSpec.label}" is ready to execute.`,
|
|
1553
|
+
emittedBy: params.emittedBy,
|
|
1554
|
+
})
|
|
1555
|
+
changed = true
|
|
1556
|
+
}
|
|
1229
1557
|
}
|
|
1230
1558
|
|
|
1231
1559
|
const readyStructuralNodes = sortedNodeSpecs.filter((nodeSpec) => {
|
|
@@ -1268,8 +1596,12 @@ class PlanExecutorService {
|
|
|
1268
1596
|
const nodeRunsById = getNodeRunsById()
|
|
1269
1597
|
const activeRunningNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'running')
|
|
1270
1598
|
const activeHumanNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'awaiting-human')
|
|
1599
|
+
const activeMonitoringNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'monitoring')
|
|
1600
|
+
const hasScheduledOrMonitoring = currentNodeRuns.some(
|
|
1601
|
+
(nodeRun) => nodeRun.status === 'scheduled' || nodeRun.status === 'monitoring',
|
|
1602
|
+
)
|
|
1271
1603
|
|
|
1272
|
-
if (!activeRunningNode && !activeHumanNode) {
|
|
1604
|
+
if (!activeRunningNode && !activeHumanNode && !activeMonitoringNode) {
|
|
1273
1605
|
const nextHumanNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
|
|
1274
1606
|
const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
|
|
1275
1607
|
return nodeRun?.status === 'ready' && isHumanNodeType(nodeSpec.type)
|
|
@@ -1384,6 +1716,16 @@ class PlanExecutorService {
|
|
|
1384
1716
|
message: `Run "${params.spec.title}" completed.`,
|
|
1385
1717
|
emittedBy: params.emittedBy,
|
|
1386
1718
|
})
|
|
1719
|
+
} else if (hasScheduledOrMonitoring) {
|
|
1720
|
+
// Nodes are waiting on schedules/monitors — run stays active
|
|
1721
|
+
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1722
|
+
status: 'running',
|
|
1723
|
+
currentNodeId: null,
|
|
1724
|
+
waitingNodeId: null,
|
|
1725
|
+
readyNodeIds: currentNodeRuns
|
|
1726
|
+
.filter((candidate) => candidate.status === 'ready')
|
|
1727
|
+
.map((candidate) => candidate.nodeId),
|
|
1728
|
+
})
|
|
1387
1729
|
} else {
|
|
1388
1730
|
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1389
1731
|
status: 'blocked',
|
|
@@ -1398,7 +1740,7 @@ class PlanExecutorService {
|
|
|
1398
1740
|
} else {
|
|
1399
1741
|
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1400
1742
|
status: activeHumanNode ? 'awaiting-human' : 'running',
|
|
1401
|
-
currentNodeId: activeHumanNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
|
|
1743
|
+
currentNodeId: activeHumanNode?.nodeId ?? activeMonitoringNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
|
|
1402
1744
|
waitingNodeId: activeHumanNode?.nodeId ?? null,
|
|
1403
1745
|
readyNodeIds: currentNodeRuns
|
|
1404
1746
|
.filter((candidate) => candidate.status === 'ready')
|
|
@@ -1556,6 +1898,7 @@ class PlanExecutorService {
|
|
|
1556
1898
|
artifacts: Array<{ id: RecordIdInput; nodeId: string }>
|
|
1557
1899
|
sequence: number
|
|
1558
1900
|
reason: string
|
|
1901
|
+
includeWorkspace?: boolean
|
|
1559
1902
|
}) {
|
|
1560
1903
|
const checkpoint = await planCheckpointService.createCheckpoint({
|
|
1561
1904
|
tx: params.tx,
|
|
@@ -1576,6 +1919,7 @@ class PlanExecutorService {
|
|
|
1576
1919
|
readyNodeIds: params.run.readyNodeIds,
|
|
1577
1920
|
nodeStatuses: Object.fromEntries(params.nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun.status])),
|
|
1578
1921
|
},
|
|
1922
|
+
includeWorkspace: params.includeWorkspace,
|
|
1579
1923
|
})
|
|
1580
1924
|
|
|
1581
1925
|
await this.emitEvent({
|