@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
|
@@ -21,39 +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'
|
|
31
|
+
import { isRecord } from '../utils/string'
|
|
32
|
+
import { feedbackLoopService } from './feedback-loop.service'
|
|
33
|
+
import { institutionalMemoryService } from './institutional-memory.service'
|
|
29
34
|
import { planApprovalService } from './plan-approval.service'
|
|
30
35
|
import { planArtifactService } from './plan-artifact.service'
|
|
31
36
|
import { planCheckpointService } from './plan-checkpoint.service'
|
|
37
|
+
import { planCoordinationService } from './plan-coordination.service'
|
|
38
|
+
import { readPathValue } from './plan-helpers'
|
|
32
39
|
import { planRunService } from './plan-run.service'
|
|
40
|
+
import { planSchedulerService } from './plan-scheduler.service'
|
|
33
41
|
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
34
42
|
import { planValidatorService } from './plan-validator.service'
|
|
43
|
+
import { qualityMetricsService } from './quality-metrics.service'
|
|
35
44
|
|
|
36
|
-
const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped'])
|
|
45
|
+
const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped', 'scheduled', 'monitoring'])
|
|
37
46
|
const HUMAN_NODE_TYPES = new Set(['human-input', 'human-approval', 'human-review-edit', 'human-decision'])
|
|
38
|
-
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join'])
|
|
39
|
-
|
|
40
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function readPathValue(source: unknown, path: string): unknown {
|
|
45
|
-
if (!path.trim()) return source
|
|
46
|
-
|
|
47
|
-
let current: unknown = source
|
|
48
|
-
for (const segment of path
|
|
49
|
-
.split('.')
|
|
50
|
-
.map((part) => part.trim())
|
|
51
|
-
.filter(Boolean)) {
|
|
52
|
-
if (!isRecord(current)) return undefined
|
|
53
|
-
current = current[segment]
|
|
54
|
-
}
|
|
55
|
-
return current
|
|
56
|
-
}
|
|
47
|
+
const STRUCTURAL_NODE_TYPES = new Set(['switch', 'join', 'deliberation-fork'])
|
|
57
48
|
|
|
58
49
|
function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
|
|
59
50
|
const segments = path
|
|
@@ -162,8 +153,8 @@ type PlanRunUpdate = Omit<
|
|
|
162
153
|
waitingNodeId?: string | null
|
|
163
154
|
replacedRunId?: RecordIdInput | null
|
|
164
155
|
lastCheckpointId?: RecordIdInput | null
|
|
165
|
-
startedAt?: Date | null
|
|
166
|
-
completedAt?: Date | null
|
|
156
|
+
startedAt?: string | Date | null
|
|
157
|
+
completedAt?: string | Date | null
|
|
167
158
|
}
|
|
168
159
|
|
|
169
160
|
type PlanNodeRunUpdate = Omit<
|
|
@@ -174,6 +165,7 @@ type PlanNodeRunUpdate = Omit<
|
|
|
174
165
|
| 'latestStructuredOutput'
|
|
175
166
|
| 'latestNotes'
|
|
176
167
|
| 'latestAttemptId'
|
|
168
|
+
| 'scheduledAt'
|
|
177
169
|
| 'readyAt'
|
|
178
170
|
| 'startedAt'
|
|
179
171
|
| 'completedAt'
|
|
@@ -184,9 +176,10 @@ type PlanNodeRunUpdate = Omit<
|
|
|
184
176
|
latestStructuredOutput?: Record<string, unknown> | null
|
|
185
177
|
latestNotes?: string | null
|
|
186
178
|
latestAttemptId?: RecordIdInput | null
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
179
|
+
scheduledAt?: string | Date | null
|
|
180
|
+
readyAt?: string | Date | null
|
|
181
|
+
startedAt?: string | Date | null
|
|
182
|
+
completedAt?: string | Date | null
|
|
190
183
|
}
|
|
191
184
|
|
|
192
185
|
function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
@@ -229,16 +222,16 @@ function toRunData(run: PlanRunRecord, patch: PlanRunUpdate) {
|
|
|
229
222
|
...(patch.startedAt === null
|
|
230
223
|
? {}
|
|
231
224
|
: patch.startedAt !== undefined
|
|
232
|
-
? { startedAt: patch.startedAt }
|
|
225
|
+
? { startedAt: toDatabaseDateTime(patch.startedAt) }
|
|
233
226
|
: run.startedAt
|
|
234
|
-
? { startedAt: run.startedAt }
|
|
227
|
+
? { startedAt: toDatabaseDateTime(run.startedAt) }
|
|
235
228
|
: {}),
|
|
236
229
|
...(patch.completedAt === null
|
|
237
230
|
? {}
|
|
238
231
|
: patch.completedAt !== undefined
|
|
239
|
-
? { completedAt: patch.completedAt }
|
|
232
|
+
? { completedAt: toDatabaseDateTime(patch.completedAt) }
|
|
240
233
|
: run.completedAt
|
|
241
|
-
? { completedAt: run.completedAt }
|
|
234
|
+
? { completedAt: toDatabaseDateTime(run.completedAt) }
|
|
242
235
|
: {}),
|
|
243
236
|
}
|
|
244
237
|
}
|
|
@@ -293,26 +286,33 @@ function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
|
|
|
293
286
|
: nodeRun.failureClass
|
|
294
287
|
? { failureClass: nodeRun.failureClass }
|
|
295
288
|
: {}),
|
|
289
|
+
...(patch.scheduledAt === null
|
|
290
|
+
? {}
|
|
291
|
+
: patch.scheduledAt !== undefined
|
|
292
|
+
? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
|
|
293
|
+
: nodeRun.scheduledAt
|
|
294
|
+
? { scheduledAt: toDatabaseDateTime(nodeRun.scheduledAt) }
|
|
295
|
+
: {}),
|
|
296
296
|
...(patch.readyAt === null
|
|
297
297
|
? {}
|
|
298
298
|
: patch.readyAt !== undefined
|
|
299
|
-
? { readyAt: patch.readyAt }
|
|
299
|
+
? { readyAt: toDatabaseDateTime(patch.readyAt) }
|
|
300
300
|
: nodeRun.readyAt
|
|
301
|
-
? { readyAt: nodeRun.readyAt }
|
|
301
|
+
? { readyAt: toDatabaseDateTime(nodeRun.readyAt) }
|
|
302
302
|
: {}),
|
|
303
303
|
...(patch.startedAt === null
|
|
304
304
|
? {}
|
|
305
305
|
: patch.startedAt !== undefined
|
|
306
|
-
? { startedAt: patch.startedAt }
|
|
306
|
+
? { startedAt: toDatabaseDateTime(patch.startedAt) }
|
|
307
307
|
: nodeRun.startedAt
|
|
308
|
-
? { startedAt: nodeRun.startedAt }
|
|
308
|
+
? { startedAt: toDatabaseDateTime(nodeRun.startedAt) }
|
|
309
309
|
: {}),
|
|
310
310
|
...(patch.completedAt === null
|
|
311
311
|
? {}
|
|
312
312
|
: patch.completedAt !== undefined
|
|
313
|
-
? { completedAt: patch.completedAt }
|
|
313
|
+
? { completedAt: toDatabaseDateTime(patch.completedAt) }
|
|
314
314
|
: nodeRun.completedAt
|
|
315
|
-
? { completedAt: nodeRun.completedAt }
|
|
315
|
+
? { completedAt: toDatabaseDateTime(nodeRun.completedAt) }
|
|
316
316
|
: {}),
|
|
317
317
|
}
|
|
318
318
|
}
|
|
@@ -767,7 +767,63 @@ class PlanExecutorService {
|
|
|
767
767
|
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
768
768
|
})
|
|
769
769
|
|
|
770
|
-
|
|
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, {
|
|
771
827
|
includeEvents: true,
|
|
772
828
|
includeArtifacts: true,
|
|
773
829
|
includeApprovals: true,
|
|
@@ -1008,7 +1064,7 @@ class PlanExecutorService {
|
|
|
1008
1064
|
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
1009
1065
|
})
|
|
1010
1066
|
|
|
1011
|
-
return
|
|
1067
|
+
return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
|
|
1012
1068
|
includeEvents: true,
|
|
1013
1069
|
includeArtifacts: true,
|
|
1014
1070
|
includeApprovals: true,
|
|
@@ -1115,6 +1171,176 @@ class PlanExecutorService {
|
|
|
1115
1171
|
})
|
|
1116
1172
|
}
|
|
1117
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
|
+
|
|
1118
1344
|
async syncRunGraph(params: {
|
|
1119
1345
|
tx: DatabaseTransaction
|
|
1120
1346
|
run: PlanRunRecord
|
|
@@ -1149,6 +1375,28 @@ class PlanExecutorService {
|
|
|
1149
1375
|
const currentArtifacts = [...params.artifacts]
|
|
1150
1376
|
const sortedNodeSpecs = [...params.nodeSpecs].sort((left, right) => left.position - right.position)
|
|
1151
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
|
+
|
|
1152
1400
|
const replaceNodeRun = (nextNodeRun: PlanNodeRunRecord) => {
|
|
1153
1401
|
currentNodeRuns = currentNodeRuns.map((candidate) =>
|
|
1154
1402
|
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
@@ -1223,25 +1471,89 @@ class PlanExecutorService {
|
|
|
1223
1471
|
}
|
|
1224
1472
|
|
|
1225
1473
|
const resolvedInput = this.buildResolvedInput({ spec: params.spec, nodeSpec, nodeRunsById, artifactsByNodeId })
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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
|
+
}
|
|
1245
1557
|
}
|
|
1246
1558
|
|
|
1247
1559
|
const readyStructuralNodes = sortedNodeSpecs.filter((nodeSpec) => {
|
|
@@ -1284,8 +1596,12 @@ class PlanExecutorService {
|
|
|
1284
1596
|
const nodeRunsById = getNodeRunsById()
|
|
1285
1597
|
const activeRunningNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'running')
|
|
1286
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
|
+
)
|
|
1287
1603
|
|
|
1288
|
-
if (!activeRunningNode && !activeHumanNode) {
|
|
1604
|
+
if (!activeRunningNode && !activeHumanNode && !activeMonitoringNode) {
|
|
1289
1605
|
const nextHumanNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
|
|
1290
1606
|
const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
|
|
1291
1607
|
return nodeRun?.status === 'ready' && isHumanNodeType(nodeSpec.type)
|
|
@@ -1400,6 +1716,16 @@ class PlanExecutorService {
|
|
|
1400
1716
|
message: `Run "${params.spec.title}" completed.`,
|
|
1401
1717
|
emittedBy: params.emittedBy,
|
|
1402
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
|
+
})
|
|
1403
1729
|
} else {
|
|
1404
1730
|
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1405
1731
|
status: 'blocked',
|
|
@@ -1414,7 +1740,7 @@ class PlanExecutorService {
|
|
|
1414
1740
|
} else {
|
|
1415
1741
|
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1416
1742
|
status: activeHumanNode ? 'awaiting-human' : 'running',
|
|
1417
|
-
currentNodeId: activeHumanNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
|
|
1743
|
+
currentNodeId: activeHumanNode?.nodeId ?? activeMonitoringNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
|
|
1418
1744
|
waitingNodeId: activeHumanNode?.nodeId ?? null,
|
|
1419
1745
|
readyNodeIds: currentNodeRuns
|
|
1420
1746
|
.filter((candidate) => candidate.status === 'ready')
|
|
@@ -1572,6 +1898,7 @@ class PlanExecutorService {
|
|
|
1572
1898
|
artifacts: Array<{ id: RecordIdInput; nodeId: string }>
|
|
1573
1899
|
sequence: number
|
|
1574
1900
|
reason: string
|
|
1901
|
+
includeWorkspace?: boolean
|
|
1575
1902
|
}) {
|
|
1576
1903
|
const checkpoint = await planCheckpointService.createCheckpoint({
|
|
1577
1904
|
tx: params.tx,
|
|
@@ -1592,6 +1919,7 @@ class PlanExecutorService {
|
|
|
1592
1919
|
readyNodeIds: params.run.readyNodeIds,
|
|
1593
1920
|
nodeStatuses: Object.fromEntries(params.nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun.status])),
|
|
1594
1921
|
},
|
|
1922
|
+
includeWorkspace: params.includeWorkspace,
|
|
1595
1923
|
})
|
|
1596
1924
|
|
|
1597
1925
|
await this.emitEvent({
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isRecord } from '../utils/string'
|
|
2
|
+
|
|
3
|
+
export function readPathValue(source: unknown, path: string): unknown {
|
|
4
|
+
if (!path.trim()) return source
|
|
5
|
+
|
|
6
|
+
let current: unknown = source
|
|
7
|
+
for (const segment of path
|
|
8
|
+
.split('.')
|
|
9
|
+
.map((part) => part.trim())
|
|
10
|
+
.filter(Boolean)) {
|
|
11
|
+
if (!isRecord(current)) return undefined
|
|
12
|
+
current = current[segment]
|
|
13
|
+
}
|
|
14
|
+
return current
|
|
15
|
+
}
|