@lota-sdk/core 0.4.8 → 0.4.10
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/package.json +11 -12
- package/src/ai/embedding-cache.ts +96 -22
- package/src/ai-gateway/ai-gateway.ts +766 -223
- package/src/config/agent-defaults.ts +189 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/background-processing.ts +1 -1
- package/src/config/constants.ts +8 -2
- package/src/config/index.ts +0 -1
- package/src/config/logger.ts +299 -19
- package/src/config/thread-defaults.ts +40 -20
- package/src/create-runtime.ts +200 -449
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-query-builder.ts +2 -1
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +868 -601
- package/src/db/memory.ts +396 -280
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +288 -0
- package/src/db/service.ts +912 -779
- package/src/db/startup.ts +153 -68
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +96 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +123 -0
- package/src/effect/index.ts +24 -0
- package/src/effect/layers.ts +238 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +46 -0
- package/src/effect/services.ts +61 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +128 -83
- package/src/index.ts +48 -1
- package/src/openrouter/direct-provider.ts +11 -35
- package/src/queues/autonomous-job.queue.ts +117 -73
- package/src/queues/context-compaction.queue.ts +50 -17
- package/src/queues/delayed-node-promotion.queue.ts +46 -17
- package/src/queues/document-processor.queue.ts +52 -77
- package/src/queues/memory-consolidation.queue.ts +47 -32
- package/src/queues/organization-learning.queue.ts +26 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +71 -24
- package/src/queues/plan-scheduler.queue.ts +97 -33
- package/src/queues/post-chat-memory.queue.ts +56 -26
- package/src/queues/queue-factory.ts +227 -59
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +45 -11
- package/src/redis/connection.ts +182 -113
- package/src/redis/index.ts +6 -8
- package/src/redis/org-memory-lock.ts +60 -27
- package/src/redis/redis-lease-lock.ts +200 -121
- package/src/redis/runtime-connection.ts +20 -0
- package/src/redis/stream-context.ts +92 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +5 -2
- package/src/runtime/agent-stream-helpers.ts +24 -9
- package/src/runtime/chat-run-orchestration.ts +102 -19
- package/src/runtime/chat-run-registry.ts +36 -2
- package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
- package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +161 -94
- package/src/runtime/domain-layer.ts +192 -0
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +16 -4
- package/src/runtime/helper-model.ts +139 -48
- package/src/runtime/index.ts +7 -8
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +50 -36
- package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
- package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +54 -67
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/memory/memory-scope.ts +53 -0
- package/src/runtime/plugin-resolution.ts +124 -25
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +177 -130
- package/src/runtime/retrieval-adapters.ts +40 -6
- package/src/runtime/runtime-accessors.ts +92 -0
- package/src/runtime/runtime-config.ts +150 -61
- package/src/runtime/runtime-extensions.ts +23 -25
- package/src/runtime/runtime-lifecycle.ts +124 -0
- package/src/runtime/runtime-services.ts +386 -0
- package/src/runtime/runtime-token.ts +47 -0
- package/src/runtime/social-chat/social-chat-agent-runner.ts +159 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +51 -20
- package/src/runtime/social-chat/social-chat.ts +630 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +433 -0
- package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
- package/src/runtime/thread-chat-helpers.ts +2 -2
- package/src/runtime/thread-plan-turn.ts +2 -1
- package/src/runtime/thread-turn-context.ts +183 -111
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +253 -149
- package/src/services/artifact.service.ts +231 -149
- package/src/services/attachment.service.ts +171 -115
- package/src/services/autonomous-job.service.ts +890 -491
- package/src/services/background-work.service.ts +54 -0
- package/src/services/chat-run-registry.service.ts +13 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +151 -88
- package/src/services/execution-plan/execution-plan-approval.ts +26 -0
- package/src/services/execution-plan/execution-plan-context.ts +29 -0
- package/src/services/execution-plan/execution-plan-graph.ts +278 -0
- package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
- package/src/services/execution-plan/execution-plan-spec.ts +75 -0
- package/src/services/execution-plan/execution-plan.service.ts +1041 -0
- package/src/services/feedback-loop.service.ts +132 -76
- package/src/services/global-orchestrator.service.ts +101 -168
- package/src/services/graph-full-routing.ts +193 -0
- package/src/services/index.ts +19 -21
- package/src/services/institutional-memory.service.ts +213 -125
- package/src/services/learned-skill.service.ts +368 -260
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-errors.ts +27 -0
- package/src/services/memory/memory-org-memory.ts +50 -0
- package/src/services/memory/memory-preseeded.ts +86 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +6 -5
- package/src/services/memory/memory.service.ts +674 -0
- package/src/services/memory/rerank.service.ts +201 -0
- package/src/services/monitoring-window.service.ts +92 -70
- package/src/services/mutating-approval.service.ts +62 -53
- package/src/services/node-workspace.service.ts +141 -98
- package/src/services/notification.service.ts +29 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +153 -77
- package/src/services/ownership-dispatcher.service.ts +456 -263
- package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
- package/src/services/plan/plan-agent-query.service.ts +322 -0
- package/src/services/{plan-approval.service.ts → plan/plan-approval.service.ts} +45 -22
- package/src/services/plan/plan-artifact.service.ts +60 -0
- package/src/services/plan/plan-builder.service.ts +76 -0
- package/src/services/plan/plan-checkpoint.service.ts +103 -0
- package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
- package/src/services/plan/plan-completion-side-effects.ts +169 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +405 -0
- package/src/services/plan/plan-deadline.service.ts +533 -0
- package/src/services/plan/plan-event-delivery.service.ts +266 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +522 -0
- package/src/services/plan/plan-executor-helpers.ts +307 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1737 -0
- package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
- package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
- package/src/services/plan/plan-run-serialization.ts +15 -0
- package/src/services/plan/plan-run.service.ts +637 -0
- package/src/services/plan/plan-scheduler.service.ts +379 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +36 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +131 -0
- package/src/services/plugin-executor.service.ts +102 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +288 -231
- package/src/services/recent-activity-title.service.ts +73 -36
- package/src/services/recent-activity.service.ts +274 -259
- package/src/services/skill-resolver.service.ts +38 -12
- package/src/services/social-chat-history.service.ts +190 -122
- package/src/services/system-executor.service.ts +96 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +385 -0
- package/src/services/thread/thread-listing.ts +199 -0
- package/src/services/thread/thread-memory-block.ts +130 -0
- package/src/services/thread/thread-message.service.ts +379 -0
- package/src/services/thread/thread-record-store.ts +155 -0
- package/src/services/thread/thread-title.service.ts +74 -0
- package/src/services/thread/thread-turn-execution.ts +280 -0
- package/src/services/thread/thread-turn-message-context.ts +73 -0
- package/src/services/thread/thread-turn-preparation.service.ts +1148 -0
- package/src/services/thread/thread-turn-streaming.ts +403 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +376 -0
- package/src/services/thread/thread.service.ts +344 -0
- package/src/services/user.service.ts +82 -32
- package/src/services/write-intent-validator.service.ts +63 -51
- package/src/storage/attachment-parser.ts +69 -27
- package/src/storage/attachment-storage.service.ts +334 -275
- package/src/storage/generated-document-storage.service.ts +66 -34
- package/src/system-agents/agent-result.ts +3 -1
- package/src/system-agents/context-compaction.agent.ts +3 -3
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/helper-agent-options.ts +1 -1
- package/src/system-agents/memory-reranker.agent.ts +3 -3
- package/src/system-agents/memory.agent.ts +3 -3
- package/src/system-agents/recent-activity-title-refiner.agent.ts +3 -3
- package/src/system-agents/regular-chat-memory-digest.agent.ts +3 -3
- package/src/system-agents/skill-extractor.agent.ts +3 -3
- package/src/system-agents/skill-manager.agent.ts +3 -3
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +3 -3
- package/src/tools/execution-plan.tool.ts +241 -171
- package/src/tools/fetch-webpage.tool.ts +29 -18
- package/src/tools/firecrawl-client.ts +26 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +57 -47
- package/src/tools/read-file-parts.tool.ts +44 -33
- package/src/tools/remember-memory.tool.ts +65 -45
- package/src/tools/search-web.tool.ts +33 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +125 -84
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +25 -22
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +111 -20
- package/src/utils/hono-error-handler.ts +24 -39
- package/src/utils/index.ts +2 -1
- package/src/utils/null-proto-record.ts +41 -0
- package/src/utils/sse-keepalive.ts +124 -21
- package/src/workers/bootstrap.ts +164 -52
- package/src/workers/memory-consolidation.worker.ts +325 -237
- package/src/workers/organization-learning.worker.ts +50 -16
- package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
- package/src/workers/regular-chat-memory-digest.runner.ts +185 -114
- package/src/workers/skill-extraction.runner.ts +176 -93
- package/src/workers/utils/file-section-chunker.ts +8 -10
- package/src/workers/utils/repo-structure-extractor.ts +349 -260
- package/src/workers/utils/repomix-file-sections.ts +2 -2
- package/src/workers/utils/thread-message-query.ts +97 -38
- package/src/workers/worker-utils.ts +74 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/config/search.ts +0 -3
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/agent-types.ts +0 -1
- package/src/runtime/context-compaction-runtime.ts +0 -87
- package/src/runtime/memory-scope.ts +0 -43
- package/src/runtime/social-chat-agent-runner.ts +0 -118
- package/src/runtime/social-chat.ts +0 -516
- package/src/runtime/team-consultation-orchestrator.ts +0 -272
- package/src/services/adaptive-playbook.service.ts +0 -152
- package/src/services/artifact-provenance.service.ts +0 -172
- package/src/services/chat-attachments.service.ts +0 -17
- package/src/services/context-compaction-runtime.singleton.ts +0 -13
- package/src/services/execution-plan.service.ts +0 -1118
- package/src/services/memory.service.ts +0 -914
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-artifact.service.ts +0 -50
- package/src/services/plan-builder.service.ts +0 -67
- package/src/services/plan-checkpoint.service.ts +0 -81
- package/src/services/plan-completion-side-effects.ts +0 -80
- package/src/services/plan-coordination.service.ts +0 -157
- package/src/services/plan-cycle.service.ts +0 -284
- package/src/services/plan-deadline.service.ts +0 -430
- package/src/services/plan-event-delivery.service.ts +0 -166
- package/src/services/plan-executor.service.ts +0 -1950
- package/src/services/plan-run.service.ts +0 -515
- package/src/services/plan-scheduler.service.ts +0 -240
- package/src/services/plan-template.service.ts +0 -177
- package/src/services/plan-validator.service.ts +0 -818
- package/src/services/plan-workspace.service.ts +0 -83
- package/src/services/rerank.service.ts +0 -156
- package/src/services/thread-message.service.ts +0 -275
- package/src/services/thread-plan-registry.service.ts +0 -22
- package/src/services/thread-title.service.ts +0 -39
- package/src/services/thread-turn-preparation.service.ts +0 -1147
- package/src/services/thread-turn.ts +0 -172
- package/src/services/thread.service.ts +0 -869
- package/src/utils/env.ts +0 -8
- /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
- /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
- /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
- /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
- /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
- /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
- /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
- /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
|
@@ -1,1950 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExecutionPlanToolResultData,
|
|
3
|
-
PlanEventRecord,
|
|
4
|
-
PlanEventType,
|
|
5
|
-
PlanFailureAction,
|
|
6
|
-
PlanFailureClass,
|
|
7
|
-
PlanNodeResultSubmission,
|
|
8
|
-
PlanNodeRunRecord,
|
|
9
|
-
PlanNodeSpecRecord,
|
|
10
|
-
PlanRunRecord,
|
|
11
|
-
PlanSpecRecord,
|
|
12
|
-
PlanValidationIssueRecord,
|
|
13
|
-
SerializableExecutionPlan,
|
|
14
|
-
} from '@lota-sdk/shared'
|
|
15
|
-
import {
|
|
16
|
-
HUMAN_NODE_TYPES as HUMAN_NODE_TYPE_VALUES,
|
|
17
|
-
PlanEventSchema,
|
|
18
|
-
PlanNodeAttemptSchema,
|
|
19
|
-
PlanNodeRunSchema,
|
|
20
|
-
PlanRunSchema,
|
|
21
|
-
STRUCTURAL_NODE_TYPES as STRUCTURAL_NODE_TYPE_VALUES,
|
|
22
|
-
PlanValidationIssueSchema,
|
|
23
|
-
} from '@lota-sdk/shared'
|
|
24
|
-
import { RecordId, StringRecordId } from 'surrealdb'
|
|
25
|
-
|
|
26
|
-
import { aiLogger } from '../config/logger'
|
|
27
|
-
import type { RecordIdInput } from '../db/record-id'
|
|
28
|
-
import { ensureRecordId, recordIdToString } from '../db/record-id'
|
|
29
|
-
import { databaseService } from '../db/service'
|
|
30
|
-
import type { DatabaseTransaction } from '../db/service'
|
|
31
|
-
import { TABLES } from '../db/tables'
|
|
32
|
-
import { generatedDocumentStorageService } from '../storage/generated-document-storage.service'
|
|
33
|
-
import { toDatabaseDateTime } from '../utils/date-time'
|
|
34
|
-
import { isRecord } from '../utils/string'
|
|
35
|
-
import { artifactService } from './artifact.service'
|
|
36
|
-
import { planApprovalService } from './plan-approval.service'
|
|
37
|
-
import { planArtifactService } from './plan-artifact.service'
|
|
38
|
-
import { planCheckpointService } from './plan-checkpoint.service'
|
|
39
|
-
import { runPlanCompletionSideEffectsSafely, runPlanNodeCompletionSideEffects } from './plan-completion-side-effects'
|
|
40
|
-
import { planCoordinationService } from './plan-coordination.service'
|
|
41
|
-
import { planEventDeliveryService } from './plan-event-delivery.service'
|
|
42
|
-
import { isExecutableConditionExpression, readPathValue } from './plan-helpers'
|
|
43
|
-
import { toPlanNodeValidationSpec } from './plan-node-spec'
|
|
44
|
-
import { buildExecutionPlanToolResult, toRunData } from './plan-run-data'
|
|
45
|
-
import type { PlanRunUpdate } from './plan-run-data'
|
|
46
|
-
import { planRunService } from './plan-run.service'
|
|
47
|
-
import { planSchedulerService } from './plan-scheduler.service'
|
|
48
|
-
import type { PlanValidationIssueInput } from './plan-validator.service'
|
|
49
|
-
import { planValidatorService } from './plan-validator.service'
|
|
50
|
-
|
|
51
|
-
const SUCCESSFUL_TERMINAL_NODE_STATUSES = new Set(['completed', 'partial', 'skipped', 'scheduled', 'monitoring'])
|
|
52
|
-
const HUMAN_NODE_TYPE_SET = new Set<string>(HUMAN_NODE_TYPE_VALUES)
|
|
53
|
-
const STRUCTURAL_NODE_TYPE_SET = new Set<string>(STRUCTURAL_NODE_TYPE_VALUES)
|
|
54
|
-
|
|
55
|
-
function setPathValue(target: Record<string, unknown>, path: string, value: unknown) {
|
|
56
|
-
const segments = path
|
|
57
|
-
.split('.')
|
|
58
|
-
.map((part) => part.trim())
|
|
59
|
-
.filter(Boolean)
|
|
60
|
-
if (segments.length === 0) return
|
|
61
|
-
const lastSegment = segments.at(-1)
|
|
62
|
-
if (!lastSegment) return
|
|
63
|
-
|
|
64
|
-
let current: Record<string, unknown> = target
|
|
65
|
-
for (const segment of segments.slice(0, -1)) {
|
|
66
|
-
const next = current[segment]
|
|
67
|
-
if (!isRecord(next)) {
|
|
68
|
-
current[segment] = {}
|
|
69
|
-
}
|
|
70
|
-
current = current[segment] as Record<string, unknown>
|
|
71
|
-
}
|
|
72
|
-
current[lastSegment] = value
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parseLiteralValue(raw: string): unknown {
|
|
76
|
-
const trimmed = raw.trim()
|
|
77
|
-
if (!trimmed.length) return undefined
|
|
78
|
-
if (trimmed === 'true') return true
|
|
79
|
-
if (trimmed === 'false') return false
|
|
80
|
-
if (trimmed === 'null') return null
|
|
81
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)
|
|
82
|
-
if (
|
|
83
|
-
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
84
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'")) ||
|
|
85
|
-
(trimmed.startsWith('`') && trimmed.endsWith('`'))
|
|
86
|
-
) {
|
|
87
|
-
return trimmed.slice(1, -1)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
return JSON.parse(trimmed)
|
|
92
|
-
} catch {
|
|
93
|
-
return trimmed
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function buildArtifactContext(artifacts: Array<{ name: string; kind: string; payload?: unknown }>) {
|
|
98
|
-
return Object.fromEntries(
|
|
99
|
-
artifacts.map((artifact) => [artifact.name, { kind: artifact.kind, payload: artifact.payload }]),
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function buildNodeContext(params: {
|
|
104
|
-
nodeRun: PlanNodeRunRecord | undefined
|
|
105
|
-
artifacts: Array<{ name: string; kind: string; payload?: unknown }>
|
|
106
|
-
}) {
|
|
107
|
-
return {
|
|
108
|
-
input: params.nodeRun?.resolvedInput ?? {},
|
|
109
|
-
output: params.nodeRun?.latestStructuredOutput ?? {},
|
|
110
|
-
artifact: buildArtifactContext(params.artifacts),
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function buildPublishedArtifactContent(params: {
|
|
115
|
-
artifact: PlanNodeResultSubmission['artifacts'][number]
|
|
116
|
-
notes: string
|
|
117
|
-
}): string {
|
|
118
|
-
const { artifact, notes } = params
|
|
119
|
-
if (artifact.content?.trim()) {
|
|
120
|
-
return artifact.content
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (artifact.payload !== undefined) {
|
|
124
|
-
return `# ${artifact.name}\n\n\`\`\`json\n${JSON.stringify(artifact.payload, null, 2)}\n\`\`\``
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return `# ${artifact.name}\n\n${notes.trim()}`
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function evaluateCondition(expression: string | undefined, context: Record<string, unknown>): boolean {
|
|
131
|
-
if (!expression?.trim()) return true
|
|
132
|
-
const normalized = expression.trim()
|
|
133
|
-
if (normalized === 'always') return true
|
|
134
|
-
|
|
135
|
-
const match = normalized.match(/^([a-zA-Z0-9_.]+)\s*(==|!=|>=|<=|>|<)\s*(.+)$/)
|
|
136
|
-
if (!match) {
|
|
137
|
-
if (!isExecutableConditionExpression(normalized)) {
|
|
138
|
-
return true
|
|
139
|
-
}
|
|
140
|
-
return Boolean(readPathValue(context, normalized))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const [, leftPath, operator, rawRightValue] = match
|
|
144
|
-
const left = readPathValue(context, leftPath)
|
|
145
|
-
const right = parseLiteralValue(rawRightValue)
|
|
146
|
-
|
|
147
|
-
if (operator === '==') return Object.is(left, right)
|
|
148
|
-
if (operator === '!=') return !Object.is(left, right)
|
|
149
|
-
if (typeof left !== 'number' || typeof right !== 'number') return false
|
|
150
|
-
if (operator === '>=') return left >= right
|
|
151
|
-
if (operator === '<=') return left <= right
|
|
152
|
-
if (operator === '>') return left > right
|
|
153
|
-
return left < right
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function isSuccessfulTerminalStatus(status: string): boolean {
|
|
157
|
-
return SUCCESSFUL_TERMINAL_NODE_STATUSES.has(status)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function isHumanNodeType(type: string): boolean {
|
|
161
|
-
return HUMAN_NODE_TYPE_SET.has(type)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isStructuralNodeType(type: string): boolean {
|
|
165
|
-
return STRUCTURAL_NODE_TYPE_SET.has(type)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
type PlanNodeRunUpdate = Omit<
|
|
169
|
-
Partial<PlanNodeRunRecord>,
|
|
170
|
-
| 'blockedReason'
|
|
171
|
-
| 'failureClass'
|
|
172
|
-
| 'resolvedInput'
|
|
173
|
-
| 'latestStructuredOutput'
|
|
174
|
-
| 'latestNotes'
|
|
175
|
-
| 'latestAttemptId'
|
|
176
|
-
| 'scheduledAt'
|
|
177
|
-
| 'readyAt'
|
|
178
|
-
| 'startedAt'
|
|
179
|
-
| 'completedAt'
|
|
180
|
-
> & {
|
|
181
|
-
blockedReason?: string | null
|
|
182
|
-
failureClass?: PlanFailureClass | null
|
|
183
|
-
resolvedInput?: Record<string, unknown> | null
|
|
184
|
-
latestStructuredOutput?: Record<string, unknown> | null
|
|
185
|
-
latestNotes?: string | null
|
|
186
|
-
latestAttemptId?: RecordIdInput | null
|
|
187
|
-
scheduledAt?: string | Date | null
|
|
188
|
-
readyAt?: string | Date | null
|
|
189
|
-
startedAt?: string | Date | null
|
|
190
|
-
completedAt?: string | Date | null
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function toNodeRunData(nodeRun: PlanNodeRunRecord, patch: PlanNodeRunUpdate) {
|
|
194
|
-
return {
|
|
195
|
-
runId: ensureRecordId(nodeRun.runId, TABLES.PLAN_RUN),
|
|
196
|
-
planSpecId: ensureRecordId(nodeRun.planSpecId, TABLES.PLAN_SPEC),
|
|
197
|
-
nodeId: nodeRun.nodeId,
|
|
198
|
-
status: patch.status ?? nodeRun.status,
|
|
199
|
-
attemptCount: patch.attemptCount ?? nodeRun.attemptCount,
|
|
200
|
-
retryCount: patch.retryCount ?? nodeRun.retryCount,
|
|
201
|
-
...(patch.resolvedInput === null
|
|
202
|
-
? {}
|
|
203
|
-
: patch.resolvedInput !== undefined
|
|
204
|
-
? { resolvedInput: patch.resolvedInput }
|
|
205
|
-
: nodeRun.resolvedInput
|
|
206
|
-
? { resolvedInput: nodeRun.resolvedInput }
|
|
207
|
-
: {}),
|
|
208
|
-
...(patch.latestStructuredOutput === null
|
|
209
|
-
? {}
|
|
210
|
-
: patch.latestStructuredOutput !== undefined
|
|
211
|
-
? { latestStructuredOutput: patch.latestStructuredOutput }
|
|
212
|
-
: nodeRun.latestStructuredOutput
|
|
213
|
-
? { latestStructuredOutput: nodeRun.latestStructuredOutput }
|
|
214
|
-
: {}),
|
|
215
|
-
...(patch.latestNotes === null
|
|
216
|
-
? {}
|
|
217
|
-
: patch.latestNotes !== undefined
|
|
218
|
-
? { latestNotes: patch.latestNotes }
|
|
219
|
-
: nodeRun.latestNotes
|
|
220
|
-
? { latestNotes: nodeRun.latestNotes }
|
|
221
|
-
: {}),
|
|
222
|
-
...(patch.latestAttemptId === null
|
|
223
|
-
? {}
|
|
224
|
-
: patch.latestAttemptId !== undefined
|
|
225
|
-
? { latestAttemptId: ensureRecordId(patch.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
|
|
226
|
-
: nodeRun.latestAttemptId
|
|
227
|
-
? { latestAttemptId: ensureRecordId(nodeRun.latestAttemptId, TABLES.PLAN_NODE_ATTEMPT) }
|
|
228
|
-
: {}),
|
|
229
|
-
...(patch.blockedReason === null
|
|
230
|
-
? {}
|
|
231
|
-
: patch.blockedReason !== undefined
|
|
232
|
-
? { blockedReason: patch.blockedReason }
|
|
233
|
-
: nodeRun.blockedReason
|
|
234
|
-
? { blockedReason: nodeRun.blockedReason }
|
|
235
|
-
: {}),
|
|
236
|
-
...(patch.failureClass === null
|
|
237
|
-
? {}
|
|
238
|
-
: patch.failureClass !== undefined
|
|
239
|
-
? { failureClass: patch.failureClass }
|
|
240
|
-
: nodeRun.failureClass
|
|
241
|
-
? { failureClass: nodeRun.failureClass }
|
|
242
|
-
: {}),
|
|
243
|
-
...(patch.scheduledAt === null
|
|
244
|
-
? {}
|
|
245
|
-
: patch.scheduledAt !== undefined
|
|
246
|
-
? { scheduledAt: toDatabaseDateTime(patch.scheduledAt) }
|
|
247
|
-
: nodeRun.scheduledAt
|
|
248
|
-
? { scheduledAt: toDatabaseDateTime(nodeRun.scheduledAt) }
|
|
249
|
-
: {}),
|
|
250
|
-
...(patch.readyAt === null
|
|
251
|
-
? {}
|
|
252
|
-
: patch.readyAt !== undefined
|
|
253
|
-
? { readyAt: toDatabaseDateTime(patch.readyAt) }
|
|
254
|
-
: nodeRun.readyAt
|
|
255
|
-
? { readyAt: toDatabaseDateTime(nodeRun.readyAt) }
|
|
256
|
-
: {}),
|
|
257
|
-
...(patch.startedAt === null
|
|
258
|
-
? {}
|
|
259
|
-
: patch.startedAt !== undefined
|
|
260
|
-
? { startedAt: toDatabaseDateTime(patch.startedAt) }
|
|
261
|
-
: nodeRun.startedAt
|
|
262
|
-
? { startedAt: toDatabaseDateTime(nodeRun.startedAt) }
|
|
263
|
-
: {}),
|
|
264
|
-
...(patch.completedAt === null
|
|
265
|
-
? {}
|
|
266
|
-
: patch.completedAt !== undefined
|
|
267
|
-
? { completedAt: toDatabaseDateTime(patch.completedAt) }
|
|
268
|
-
: nodeRun.completedAt
|
|
269
|
-
? { completedAt: toDatabaseDateTime(nodeRun.completedAt) }
|
|
270
|
-
: {}),
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function deriveApprovalStatus(response: Record<string, unknown>): 'approved' | 'rejected' | 'changes-requested' {
|
|
275
|
-
const approved = response.approved === true
|
|
276
|
-
const requiredEdits = Array.isArray(response.requiredEdits)
|
|
277
|
-
? response.requiredEdits.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
|
|
278
|
-
: []
|
|
279
|
-
|
|
280
|
-
if (approved && requiredEdits.length === 0) return 'approved'
|
|
281
|
-
if (requiredEdits.length > 0) return 'changes-requested'
|
|
282
|
-
return 'rejected'
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
class PlanExecutorService {
|
|
286
|
-
async submitNodeResult(params: {
|
|
287
|
-
threadId: RecordIdInput
|
|
288
|
-
runId: string
|
|
289
|
-
nodeId: string
|
|
290
|
-
emittedBy: string
|
|
291
|
-
result: PlanNodeResultSubmission
|
|
292
|
-
}): Promise<ExecutionPlanToolResultData> {
|
|
293
|
-
const run = await planRunService.getRunById(params.runId)
|
|
294
|
-
if (recordIdToString(run.threadId, TABLES.THREAD) !== recordIdToString(params.threadId, TABLES.THREAD)) {
|
|
295
|
-
throw new Error('Execution node result targets a different thread.')
|
|
296
|
-
}
|
|
297
|
-
if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
|
|
298
|
-
throw new Error('Execution run is no longer active.')
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
302
|
-
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
303
|
-
const nodeSpec = nodeSpecs.find((candidate) => candidate.nodeId === params.nodeId)
|
|
304
|
-
if (!nodeSpec) {
|
|
305
|
-
throw new Error(`Execution node "${params.nodeId}" does not exist in this run.`)
|
|
306
|
-
}
|
|
307
|
-
if (isHumanNodeType(nodeSpec.type) || isStructuralNodeType(nodeSpec.type)) {
|
|
308
|
-
throw new Error(
|
|
309
|
-
`Execution node "${nodeSpec.label}" is executor-owned and cannot accept direct result submission.`,
|
|
310
|
-
)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
314
|
-
if (nodeRun.status !== 'running') {
|
|
315
|
-
throw new Error(`Execution node "${nodeSpec.label}" is not currently running.`)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const existingArtifacts = await planRunService.listArtifacts(run.id)
|
|
319
|
-
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
320
|
-
const validation = planValidatorService.validateNodeResult({
|
|
321
|
-
draft: { schemas: spec.schemaRegistry },
|
|
322
|
-
node: toPlanNodeValidationSpec(nodeSpec),
|
|
323
|
-
result: params.result,
|
|
324
|
-
})
|
|
325
|
-
const emittedEvents: PlanEventRecord[] = []
|
|
326
|
-
const publishedArtifactStorageKeys: string[] = []
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
await databaseService.withTransaction(async (tx) => {
|
|
330
|
-
const attempt = await this.createAttempt({
|
|
331
|
-
tx,
|
|
332
|
-
run,
|
|
333
|
-
nodeRun,
|
|
334
|
-
emittedBy: params.emittedBy,
|
|
335
|
-
result: params.result,
|
|
336
|
-
status: validation.blocking.length > 0 ? 'failed' : 'completed',
|
|
337
|
-
failureClass: validation.failureClass,
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
const issues = await this.persistValidationIssues({
|
|
341
|
-
tx,
|
|
342
|
-
run,
|
|
343
|
-
spec,
|
|
344
|
-
attemptId: attempt.id,
|
|
345
|
-
nodeId: params.nodeId,
|
|
346
|
-
issues: [...validation.blocking, ...validation.warnings],
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
const finalizedAttempt = PlanNodeAttemptSchema.parse(
|
|
350
|
-
await tx
|
|
351
|
-
.update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
|
|
352
|
-
.content({
|
|
353
|
-
runId: ensureRecordId(attempt.runId, TABLES.PLAN_RUN),
|
|
354
|
-
nodeRunId: ensureRecordId(attempt.nodeRunId, TABLES.PLAN_NODE_RUN),
|
|
355
|
-
nodeId: attempt.nodeId,
|
|
356
|
-
emittedBy: attempt.emittedBy,
|
|
357
|
-
status: attempt.status,
|
|
358
|
-
...(attempt.structuredOutput ? { structuredOutput: attempt.structuredOutput } : {}),
|
|
359
|
-
...(attempt.notes ? { notes: attempt.notes } : {}),
|
|
360
|
-
validationIssueIds: issues.map((issue) => ensureRecordId(issue.id, TABLES.PLAN_VALIDATION_ISSUE)),
|
|
361
|
-
...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
|
|
362
|
-
})
|
|
363
|
-
.output('after'),
|
|
364
|
-
)
|
|
365
|
-
|
|
366
|
-
const publishedArtifacts =
|
|
367
|
-
validation.blocking.length > 0
|
|
368
|
-
? params.result.artifacts
|
|
369
|
-
: await Promise.all(
|
|
370
|
-
params.result.artifacts.map(async (artifact) => {
|
|
371
|
-
const deliverable = nodeSpec.deliverables.find((candidate) => candidate.name === artifact.name)
|
|
372
|
-
if (!deliverable?.publishAs) {
|
|
373
|
-
return artifact
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const content = buildPublishedArtifactContent({ artifact, notes: params.result.notes })
|
|
377
|
-
const published = await artifactService.publishArtifactInTransaction(
|
|
378
|
-
{
|
|
379
|
-
organizationId: recordIdToString(run.organizationId, TABLES.ORGANIZATION),
|
|
380
|
-
authorAgentId: params.emittedBy,
|
|
381
|
-
title: artifact.name,
|
|
382
|
-
artifactKind: deliverable.publishAs.artifactKind,
|
|
383
|
-
templateId: deliverable.publishAs.templateId,
|
|
384
|
-
canonicalKey: deliverable.publishAs.canonicalKey,
|
|
385
|
-
content,
|
|
386
|
-
tags: [],
|
|
387
|
-
...(artifact.description ? { description: artifact.description } : {}),
|
|
388
|
-
sourceThreadId: recordIdToString(run.threadId, TABLES.THREAD),
|
|
389
|
-
sourcePlanRunId: recordIdToString(run.id, TABLES.PLAN_RUN),
|
|
390
|
-
sourcePlanNodeId: params.nodeId,
|
|
391
|
-
deliverableName: artifact.name,
|
|
392
|
-
},
|
|
393
|
-
tx,
|
|
394
|
-
)
|
|
395
|
-
publishedArtifactStorageKeys.push(published.storageKey)
|
|
396
|
-
|
|
397
|
-
return { ...artifact, content, publishedArtifactId: recordIdToString(published.id, TABLES.ARTIFACT) }
|
|
398
|
-
}),
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
const persistedArtifacts = await planArtifactService.persistArtifacts({
|
|
402
|
-
tx,
|
|
403
|
-
runId: run.id,
|
|
404
|
-
attemptId: finalizedAttempt.id,
|
|
405
|
-
nodeId: params.nodeId,
|
|
406
|
-
artifacts: publishedArtifacts,
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
let nextNodeRun = PlanNodeRunSchema.parse(
|
|
410
|
-
await tx
|
|
411
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
412
|
-
.content(
|
|
413
|
-
toNodeRunData(nodeRun, {
|
|
414
|
-
attemptCount: nodeRun.attemptCount + 1,
|
|
415
|
-
latestAttemptId: finalizedAttempt.id,
|
|
416
|
-
latestStructuredOutput: params.result.structuredOutput ?? null,
|
|
417
|
-
latestNotes: params.result.notes,
|
|
418
|
-
}),
|
|
419
|
-
)
|
|
420
|
-
.output('after'),
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
424
|
-
const withUpdatedNodeRuns = nodeRuns.map((candidate) =>
|
|
425
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
426
|
-
)
|
|
427
|
-
const nextArtifacts = [...existingArtifacts, ...persistedArtifacts]
|
|
428
|
-
|
|
429
|
-
if (validation.blocking.length > 0) {
|
|
430
|
-
const shouldRetry =
|
|
431
|
-
validation.failureClass &&
|
|
432
|
-
nodeSpec.retryPolicy.maxAttempts > nextNodeRun.retryCount &&
|
|
433
|
-
(nodeSpec.retryPolicy.retryOn.length === 0 ||
|
|
434
|
-
nodeSpec.retryPolicy.retryOn.includes(validation.failureClass))
|
|
435
|
-
|
|
436
|
-
if (shouldRetry) {
|
|
437
|
-
nextNodeRun = PlanNodeRunSchema.parse(
|
|
438
|
-
await tx
|
|
439
|
-
.update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
440
|
-
.content(
|
|
441
|
-
toNodeRunData(nextNodeRun, {
|
|
442
|
-
status: 'ready',
|
|
443
|
-
retryCount: nextNodeRun.retryCount + 1,
|
|
444
|
-
failureClass: validation.failureClass,
|
|
445
|
-
blockedReason: validation.blocking[0]?.message ?? null,
|
|
446
|
-
readyAt: new Date(),
|
|
447
|
-
startedAt: null,
|
|
448
|
-
completedAt: null,
|
|
449
|
-
}),
|
|
450
|
-
)
|
|
451
|
-
.output('after'),
|
|
452
|
-
)
|
|
453
|
-
|
|
454
|
-
await this.emitEvent({
|
|
455
|
-
tx,
|
|
456
|
-
run,
|
|
457
|
-
spec,
|
|
458
|
-
nodeId: nextNodeRun.nodeId,
|
|
459
|
-
attemptId: finalizedAttempt.id,
|
|
460
|
-
eventType: 'validation-reported',
|
|
461
|
-
message: `Validation failed for node "${nodeSpec.label}", scheduling retry.`,
|
|
462
|
-
detail: { issues: validation.blocking.map((issue) => issue.code) },
|
|
463
|
-
emittedBy: params.emittedBy,
|
|
464
|
-
capturedEvents: emittedEvents,
|
|
465
|
-
})
|
|
466
|
-
await this.emitEvent({
|
|
467
|
-
tx,
|
|
468
|
-
run,
|
|
469
|
-
spec,
|
|
470
|
-
nodeId: nextNodeRun.nodeId,
|
|
471
|
-
attemptId: finalizedAttempt.id,
|
|
472
|
-
eventType: 'node-unblocked',
|
|
473
|
-
fromStatus: nodeRun.status,
|
|
474
|
-
toStatus: nextNodeRun.status,
|
|
475
|
-
message: `Node "${nodeSpec.label}" is ready for another attempt.`,
|
|
476
|
-
detail: { retryCount: nextNodeRun.retryCount },
|
|
477
|
-
emittedBy: params.emittedBy,
|
|
478
|
-
capturedEvents: emittedEvents,
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
const synced = await this.syncRunGraph({
|
|
482
|
-
tx,
|
|
483
|
-
run,
|
|
484
|
-
spec,
|
|
485
|
-
nodeSpecs,
|
|
486
|
-
nodeRuns: withUpdatedNodeRuns.map((candidate) =>
|
|
487
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
488
|
-
),
|
|
489
|
-
artifacts: nextArtifacts,
|
|
490
|
-
emittedBy: params.emittedBy,
|
|
491
|
-
capturedEvents: emittedEvents,
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
const checkpoint = await this.saveCheckpoint({
|
|
495
|
-
tx,
|
|
496
|
-
run: synced.run,
|
|
497
|
-
spec,
|
|
498
|
-
nodeRuns: synced.nodeRuns,
|
|
499
|
-
artifacts: synced.artifacts,
|
|
500
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
501
|
-
reason: 'node-result-retry',
|
|
502
|
-
capturedEvents: emittedEvents,
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
506
|
-
return
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const failureAction = this.resolveFailureAction(nodeSpec, validation.failureClass)
|
|
510
|
-
if (failureAction === 'human-review') {
|
|
511
|
-
nextNodeRun = PlanNodeRunSchema.parse(
|
|
512
|
-
await tx
|
|
513
|
-
.update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
514
|
-
.content(
|
|
515
|
-
toNodeRunData(nextNodeRun, {
|
|
516
|
-
status: 'awaiting-human',
|
|
517
|
-
retryCount: nextNodeRun.retryCount + 1,
|
|
518
|
-
failureClass: validation.failureClass,
|
|
519
|
-
blockedReason: validation.blocking[0]?.message ?? null,
|
|
520
|
-
startedAt: nextNodeRun.startedAt ?? new Date(),
|
|
521
|
-
}),
|
|
522
|
-
)
|
|
523
|
-
.output('after'),
|
|
524
|
-
)
|
|
525
|
-
|
|
526
|
-
const approval = await planApprovalService.createPendingApproval({
|
|
527
|
-
tx,
|
|
528
|
-
runId: run.id,
|
|
529
|
-
nodeRunId: nextNodeRun.id,
|
|
530
|
-
nodeId: nextNodeRun.nodeId,
|
|
531
|
-
requestedBy: params.emittedBy,
|
|
532
|
-
presented: {
|
|
533
|
-
nodeId: nodeSpec.nodeId,
|
|
534
|
-
label: nodeSpec.label,
|
|
535
|
-
objective: nodeSpec.objective,
|
|
536
|
-
instructions: nodeSpec.instructions,
|
|
537
|
-
validationIssues: validation.blocking,
|
|
538
|
-
},
|
|
539
|
-
})
|
|
540
|
-
|
|
541
|
-
const failedRun = await this.replaceRun(tx, run, {
|
|
542
|
-
status: 'awaiting-human',
|
|
543
|
-
currentNodeId: nextNodeRun.nodeId,
|
|
544
|
-
waitingNodeId: nextNodeRun.nodeId,
|
|
545
|
-
readyNodeIds: [],
|
|
546
|
-
failureCount: run.failureCount + 1,
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
await this.emitEvent({
|
|
550
|
-
tx,
|
|
551
|
-
run: failedRun,
|
|
552
|
-
spec,
|
|
553
|
-
nodeId: nextNodeRun.nodeId,
|
|
554
|
-
attemptId: finalizedAttempt.id,
|
|
555
|
-
approvalId: approval.id,
|
|
556
|
-
eventType: 'approval-requested',
|
|
557
|
-
fromStatus: run.status,
|
|
558
|
-
toStatus: failedRun.status,
|
|
559
|
-
message: `Node "${nodeSpec.label}" requires human review before continuing.`,
|
|
560
|
-
detail: { issues: validation.blocking.map((issue) => issue.code) },
|
|
561
|
-
emittedBy: params.emittedBy,
|
|
562
|
-
capturedEvents: emittedEvents,
|
|
563
|
-
})
|
|
564
|
-
|
|
565
|
-
const checkpoint = await this.saveCheckpoint({
|
|
566
|
-
tx,
|
|
567
|
-
run: failedRun,
|
|
568
|
-
spec,
|
|
569
|
-
nodeRuns: withUpdatedNodeRuns.map((candidate) =>
|
|
570
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
571
|
-
),
|
|
572
|
-
artifacts: nextArtifacts,
|
|
573
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
574
|
-
reason: 'node-result-human-review',
|
|
575
|
-
capturedEvents: emittedEvents,
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
await this.attachCheckpoint(tx, failedRun, checkpoint)
|
|
579
|
-
return
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (failureAction === 'replan') {
|
|
583
|
-
nextNodeRun = PlanNodeRunSchema.parse(
|
|
584
|
-
await tx
|
|
585
|
-
.update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
586
|
-
.content(
|
|
587
|
-
toNodeRunData(nextNodeRun, {
|
|
588
|
-
status: 'blocked',
|
|
589
|
-
retryCount: nextNodeRun.retryCount + 1,
|
|
590
|
-
failureClass: validation.failureClass,
|
|
591
|
-
blockedReason: validation.blocking[0]?.message ?? null,
|
|
592
|
-
}),
|
|
593
|
-
)
|
|
594
|
-
.output('after'),
|
|
595
|
-
)
|
|
596
|
-
|
|
597
|
-
const blockedRun = await this.replaceRun(tx, run, {
|
|
598
|
-
status: 'blocked',
|
|
599
|
-
currentNodeId: nextNodeRun.nodeId,
|
|
600
|
-
waitingNodeId: null,
|
|
601
|
-
readyNodeIds: [],
|
|
602
|
-
failureCount: run.failureCount + 1,
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
await this.emitEvent({
|
|
606
|
-
tx,
|
|
607
|
-
run: blockedRun,
|
|
608
|
-
spec,
|
|
609
|
-
nodeId: nextNodeRun.nodeId,
|
|
610
|
-
attemptId: finalizedAttempt.id,
|
|
611
|
-
eventType: 'node-blocked',
|
|
612
|
-
fromStatus: nodeRun.status,
|
|
613
|
-
toStatus: nextNodeRun.status,
|
|
614
|
-
message: `Node "${nodeSpec.label}" failed validation and requires replanning.`,
|
|
615
|
-
detail: { failureClass: validation.failureClass, issues: validation.blocking.map((issue) => issue.code) },
|
|
616
|
-
emittedBy: params.emittedBy,
|
|
617
|
-
capturedEvents: emittedEvents,
|
|
618
|
-
})
|
|
619
|
-
|
|
620
|
-
const checkpoint = await this.saveCheckpoint({
|
|
621
|
-
tx,
|
|
622
|
-
run: blockedRun,
|
|
623
|
-
spec,
|
|
624
|
-
nodeRuns: withUpdatedNodeRuns.map((candidate) =>
|
|
625
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
626
|
-
),
|
|
627
|
-
artifacts: nextArtifacts,
|
|
628
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
629
|
-
reason: 'node-result-replan',
|
|
630
|
-
capturedEvents: emittedEvents,
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
await this.attachCheckpoint(tx, blockedRun, checkpoint)
|
|
634
|
-
return
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
nextNodeRun = PlanNodeRunSchema.parse(
|
|
638
|
-
await tx
|
|
639
|
-
.update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
640
|
-
.content(
|
|
641
|
-
toNodeRunData(nextNodeRun, {
|
|
642
|
-
status: 'failed',
|
|
643
|
-
retryCount: nextNodeRun.retryCount + 1,
|
|
644
|
-
failureClass: validation.failureClass,
|
|
645
|
-
blockedReason: validation.blocking[0]?.message ?? null,
|
|
646
|
-
completedAt: new Date(),
|
|
647
|
-
}),
|
|
648
|
-
)
|
|
649
|
-
.output('after'),
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
const failedRun = await this.replaceRun(tx, run, {
|
|
653
|
-
status: 'failed',
|
|
654
|
-
currentNodeId: null,
|
|
655
|
-
waitingNodeId: null,
|
|
656
|
-
readyNodeIds: [],
|
|
657
|
-
failureCount: run.failureCount + 1,
|
|
658
|
-
completedAt: new Date(),
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
await this.emitEvent({
|
|
662
|
-
tx,
|
|
663
|
-
run: failedRun,
|
|
664
|
-
spec,
|
|
665
|
-
nodeId: nextNodeRun.nodeId,
|
|
666
|
-
attemptId: finalizedAttempt.id,
|
|
667
|
-
eventType: 'node-failed',
|
|
668
|
-
fromStatus: nodeRun.status,
|
|
669
|
-
toStatus: nextNodeRun.status,
|
|
670
|
-
message: `Node "${nodeSpec.label}" failed validation and the run has been aborted.`,
|
|
671
|
-
detail: { failureClass: validation.failureClass, issues: validation.blocking.map((issue) => issue.code) },
|
|
672
|
-
emittedBy: params.emittedBy,
|
|
673
|
-
capturedEvents: emittedEvents,
|
|
674
|
-
})
|
|
675
|
-
|
|
676
|
-
const checkpoint = await this.saveCheckpoint({
|
|
677
|
-
tx,
|
|
678
|
-
run: failedRun,
|
|
679
|
-
spec,
|
|
680
|
-
nodeRuns: withUpdatedNodeRuns.map((candidate) =>
|
|
681
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
682
|
-
),
|
|
683
|
-
artifacts: nextArtifacts,
|
|
684
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
685
|
-
reason: 'node-result-failed',
|
|
686
|
-
capturedEvents: emittedEvents,
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
await this.attachCheckpoint(tx, failedRun, checkpoint)
|
|
690
|
-
return
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
nextNodeRun = PlanNodeRunSchema.parse(
|
|
694
|
-
await tx
|
|
695
|
-
.update(ensureRecordId(nextNodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
696
|
-
.content(
|
|
697
|
-
toNodeRunData(nextNodeRun, {
|
|
698
|
-
status: validation.warnings.length > 0 ? 'partial' : 'completed',
|
|
699
|
-
latestAttemptId: finalizedAttempt.id,
|
|
700
|
-
latestStructuredOutput: params.result.structuredOutput ?? null,
|
|
701
|
-
latestNotes: params.result.notes,
|
|
702
|
-
blockedReason: null,
|
|
703
|
-
failureClass: null,
|
|
704
|
-
completedAt: new Date(),
|
|
705
|
-
}),
|
|
706
|
-
)
|
|
707
|
-
.output('after'),
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
await this.emitEvent({
|
|
711
|
-
tx,
|
|
712
|
-
run,
|
|
713
|
-
spec,
|
|
714
|
-
nodeId: nextNodeRun.nodeId,
|
|
715
|
-
attemptId: finalizedAttempt.id,
|
|
716
|
-
eventType: validation.warnings.length > 0 ? 'node-partial' : 'node-completed',
|
|
717
|
-
fromStatus: nodeRun.status,
|
|
718
|
-
toStatus: nextNodeRun.status,
|
|
719
|
-
message:
|
|
720
|
-
validation.warnings.length > 0
|
|
721
|
-
? `Node "${nodeSpec.label}" completed with warnings.`
|
|
722
|
-
: `Node "${nodeSpec.label}" completed successfully.`,
|
|
723
|
-
detail: { warningCount: validation.warnings.length },
|
|
724
|
-
emittedBy: params.emittedBy,
|
|
725
|
-
capturedEvents: emittedEvents,
|
|
726
|
-
})
|
|
727
|
-
|
|
728
|
-
const synced = await this.syncRunGraph({
|
|
729
|
-
tx,
|
|
730
|
-
run,
|
|
731
|
-
spec,
|
|
732
|
-
nodeSpecs,
|
|
733
|
-
nodeRuns: withUpdatedNodeRuns.map((candidate) =>
|
|
734
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
735
|
-
),
|
|
736
|
-
artifacts: nextArtifacts,
|
|
737
|
-
emittedBy: params.emittedBy,
|
|
738
|
-
capturedEvents: emittedEvents,
|
|
739
|
-
})
|
|
740
|
-
|
|
741
|
-
const checkpoint = await this.saveCheckpoint({
|
|
742
|
-
tx,
|
|
743
|
-
run: synced.run,
|
|
744
|
-
spec,
|
|
745
|
-
nodeRuns: synced.nodeRuns,
|
|
746
|
-
artifacts: synced.artifacts,
|
|
747
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
748
|
-
reason: 'node-result-complete',
|
|
749
|
-
capturedEvents: emittedEvents,
|
|
750
|
-
})
|
|
751
|
-
|
|
752
|
-
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
753
|
-
})
|
|
754
|
-
} catch (error) {
|
|
755
|
-
await Promise.allSettled(
|
|
756
|
-
publishedArtifactStorageKeys.map((storageKey) =>
|
|
757
|
-
generatedDocumentStorageService.deleteTextArtifact(storageKey),
|
|
758
|
-
),
|
|
759
|
-
)
|
|
760
|
-
throw error
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
const orgId = recordIdToString(run.organizationId, TABLES.ORGANIZATION)
|
|
764
|
-
const runIdStr = recordIdToString(run.id, TABLES.PLAN_RUN)
|
|
765
|
-
void runPlanNodeCompletionSideEffects({
|
|
766
|
-
runId: runIdStr,
|
|
767
|
-
organizationId: orgId,
|
|
768
|
-
nodeId: params.nodeId,
|
|
769
|
-
nodeLabel: nodeSpec.label,
|
|
770
|
-
nodeOwnerRef: nodeSpec.owner.ref,
|
|
771
|
-
nodeOwnerType: nodeSpec.owner.executorType,
|
|
772
|
-
nodeType: nodeSpec.type,
|
|
773
|
-
nodeStartedAt: nodeRun.startedAt,
|
|
774
|
-
nodeAttemptCount: nodeRun.attemptCount + 1,
|
|
775
|
-
artifactCount: params.result.artifacts.length,
|
|
776
|
-
validationIssues: [...validation.blocking, ...validation.warnings],
|
|
777
|
-
}).catch((error) => {
|
|
778
|
-
aiLogger.warn`Failed to record node completion metrics for run ${runIdStr} node ${params.nodeId}: ${error instanceof Error ? error.message : String(error)}`
|
|
779
|
-
})
|
|
780
|
-
|
|
781
|
-
const updatedRun = await planRunService.getRunById(run.id)
|
|
782
|
-
if (updatedRun.status === 'completed') {
|
|
783
|
-
void runPlanCompletionSideEffectsSafely({ runId: runIdStr, organizationId: orgId })
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
787
|
-
|
|
788
|
-
const snapshot = await planRunService.toSerializablePlan(updatedRun, {
|
|
789
|
-
includeEvents: true,
|
|
790
|
-
includeArtifacts: true,
|
|
791
|
-
includeApprovals: true,
|
|
792
|
-
includeCheckpoints: true,
|
|
793
|
-
includeValidationIssues: true,
|
|
794
|
-
})
|
|
795
|
-
|
|
796
|
-
return buildExecutionPlanToolResult({
|
|
797
|
-
action: 'node-result-submitted',
|
|
798
|
-
plan: snapshot,
|
|
799
|
-
message: `Submitted result for node "${nodeSpec.label}".`,
|
|
800
|
-
})
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
async submitHumanNodeResponse(params: {
|
|
804
|
-
threadId: RecordIdInput
|
|
805
|
-
approvalId?: string
|
|
806
|
-
respondedBy: string
|
|
807
|
-
response: Record<string, unknown>
|
|
808
|
-
approvalMessageId?: string
|
|
809
|
-
}): Promise<SerializableExecutionPlan | null> {
|
|
810
|
-
const run = await planRunService.getActiveRunRecord(params.threadId)
|
|
811
|
-
if (!run || run.status !== 'awaiting-human' || !run.waitingNodeId) {
|
|
812
|
-
return null
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
816
|
-
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
817
|
-
const nodeSpec = nodeSpecs.find((candidate) => candidate.nodeId === run.waitingNodeId)
|
|
818
|
-
if (!nodeSpec) {
|
|
819
|
-
throw new Error(`Waiting node "${run.waitingNodeId}" does not exist.`)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, run.waitingNodeId)
|
|
823
|
-
const approval =
|
|
824
|
-
(params.approvalId ? await planApprovalService.getApprovalById(params.approvalId) : null) ??
|
|
825
|
-
(await planApprovalService.getPendingApprovalForNodeRun(nodeRun.id))
|
|
826
|
-
if (!approval) {
|
|
827
|
-
throw new Error(`No pending approval exists for node "${nodeSpec.label}".`)
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const existingArtifacts = await planRunService.listArtifacts(run.id)
|
|
831
|
-
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
832
|
-
const validation = planValidatorService.validateNodeResult({
|
|
833
|
-
draft: { schemas: spec.schemaRegistry },
|
|
834
|
-
node: toPlanNodeValidationSpec(nodeSpec),
|
|
835
|
-
result: {
|
|
836
|
-
structuredOutput: params.response,
|
|
837
|
-
artifacts: [],
|
|
838
|
-
notes: typeof params.response.comments === 'string' ? params.response.comments : 'Human response submitted.',
|
|
839
|
-
},
|
|
840
|
-
})
|
|
841
|
-
const emittedEvents: PlanEventRecord[] = []
|
|
842
|
-
|
|
843
|
-
await databaseService.withTransaction(async (tx) => {
|
|
844
|
-
const approvalStatus = deriveApprovalStatus(params.response)
|
|
845
|
-
await planApprovalService.updateApprovalResponse({
|
|
846
|
-
tx,
|
|
847
|
-
approval,
|
|
848
|
-
status: approvalStatus,
|
|
849
|
-
response: params.response,
|
|
850
|
-
respondedBy: params.respondedBy,
|
|
851
|
-
approvalMessageId: params.approvalMessageId,
|
|
852
|
-
comments: typeof params.response.comments === 'string' ? params.response.comments : undefined,
|
|
853
|
-
requiredEdits: Array.isArray(params.response.requiredEdits)
|
|
854
|
-
? params.response.requiredEdits.filter((entry): entry is string => typeof entry === 'string')
|
|
855
|
-
: undefined,
|
|
856
|
-
})
|
|
857
|
-
|
|
858
|
-
const attempt = await this.createAttempt({
|
|
859
|
-
tx,
|
|
860
|
-
run,
|
|
861
|
-
nodeRun,
|
|
862
|
-
emittedBy: params.respondedBy,
|
|
863
|
-
result: {
|
|
864
|
-
structuredOutput: params.response,
|
|
865
|
-
artifacts: [],
|
|
866
|
-
notes: typeof params.response.comments === 'string' ? params.response.comments : 'Human response submitted.',
|
|
867
|
-
},
|
|
868
|
-
status: validation.blocking.length > 0 ? 'failed' : 'completed',
|
|
869
|
-
failureClass: validation.failureClass,
|
|
870
|
-
})
|
|
871
|
-
|
|
872
|
-
const issues = await this.persistValidationIssues({
|
|
873
|
-
tx,
|
|
874
|
-
run,
|
|
875
|
-
spec,
|
|
876
|
-
attemptId: attempt.id,
|
|
877
|
-
nodeId: nodeRun.nodeId,
|
|
878
|
-
issues: [...validation.blocking, ...validation.warnings],
|
|
879
|
-
})
|
|
880
|
-
|
|
881
|
-
await tx
|
|
882
|
-
.update(ensureRecordId(attempt.id, TABLES.PLAN_NODE_ATTEMPT))
|
|
883
|
-
.content({
|
|
884
|
-
runId: ensureRecordId(attempt.runId, TABLES.PLAN_RUN),
|
|
885
|
-
nodeRunId: ensureRecordId(attempt.nodeRunId, TABLES.PLAN_NODE_RUN),
|
|
886
|
-
nodeId: attempt.nodeId,
|
|
887
|
-
emittedBy: attempt.emittedBy,
|
|
888
|
-
status: attempt.status,
|
|
889
|
-
structuredOutput: params.response,
|
|
890
|
-
validationIssueIds: issues.map((issue) => ensureRecordId(issue.id, TABLES.PLAN_VALIDATION_ISSUE)),
|
|
891
|
-
...(attempt.notes ? { notes: attempt.notes } : {}),
|
|
892
|
-
...(attempt.failureClass ? { failureClass: attempt.failureClass } : {}),
|
|
893
|
-
})
|
|
894
|
-
.output('after')
|
|
895
|
-
|
|
896
|
-
const nextNodeRun =
|
|
897
|
-
validation.blocking.length > 0
|
|
898
|
-
? PlanNodeRunSchema.parse(
|
|
899
|
-
await tx
|
|
900
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
901
|
-
.content(
|
|
902
|
-
toNodeRunData(nodeRun, {
|
|
903
|
-
status: 'blocked',
|
|
904
|
-
attemptCount: nodeRun.attemptCount + 1,
|
|
905
|
-
latestAttemptId: attempt.id,
|
|
906
|
-
latestStructuredOutput: params.response,
|
|
907
|
-
latestNotes: typeof params.response.comments === 'string' ? params.response.comments : null,
|
|
908
|
-
blockedReason: validation.blocking[0]?.message ?? null,
|
|
909
|
-
failureClass: validation.failureClass,
|
|
910
|
-
}),
|
|
911
|
-
)
|
|
912
|
-
.output('after'),
|
|
913
|
-
)
|
|
914
|
-
: PlanNodeRunSchema.parse(
|
|
915
|
-
await tx
|
|
916
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
917
|
-
.content(
|
|
918
|
-
toNodeRunData(nodeRun, {
|
|
919
|
-
status: 'completed',
|
|
920
|
-
attemptCount: nodeRun.attemptCount + 1,
|
|
921
|
-
latestAttemptId: attempt.id,
|
|
922
|
-
latestStructuredOutput: params.response,
|
|
923
|
-
latestNotes: typeof params.response.comments === 'string' ? params.response.comments : null,
|
|
924
|
-
blockedReason: null,
|
|
925
|
-
failureClass: null,
|
|
926
|
-
completedAt: new Date(),
|
|
927
|
-
}),
|
|
928
|
-
)
|
|
929
|
-
.output('after'),
|
|
930
|
-
)
|
|
931
|
-
|
|
932
|
-
const nodeRuns = (await planRunService.listNodeRuns(run.id)).map((candidate) =>
|
|
933
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
934
|
-
)
|
|
935
|
-
|
|
936
|
-
if (validation.blocking.length > 0) {
|
|
937
|
-
const blockedRun = await this.replaceRun(tx, run, {
|
|
938
|
-
status: 'blocked',
|
|
939
|
-
currentNodeId: nextNodeRun.nodeId,
|
|
940
|
-
waitingNodeId: null,
|
|
941
|
-
readyNodeIds: [],
|
|
942
|
-
failureCount: run.failureCount + 1,
|
|
943
|
-
})
|
|
944
|
-
|
|
945
|
-
await this.emitEvent({
|
|
946
|
-
tx,
|
|
947
|
-
run: blockedRun,
|
|
948
|
-
spec,
|
|
949
|
-
nodeId: nextNodeRun.nodeId,
|
|
950
|
-
attemptId: attempt.id,
|
|
951
|
-
approvalId: approval.id,
|
|
952
|
-
eventType: 'approval-resolved',
|
|
953
|
-
fromStatus: run.status,
|
|
954
|
-
toStatus: blockedRun.status,
|
|
955
|
-
message: `Human response for node "${nodeSpec.label}" blocked execution.`,
|
|
956
|
-
detail: { issues: validation.blocking.map((issue) => issue.code) },
|
|
957
|
-
emittedBy: params.respondedBy,
|
|
958
|
-
capturedEvents: emittedEvents,
|
|
959
|
-
})
|
|
960
|
-
|
|
961
|
-
const checkpoint = await this.saveCheckpoint({
|
|
962
|
-
tx,
|
|
963
|
-
run: blockedRun,
|
|
964
|
-
spec,
|
|
965
|
-
nodeRuns,
|
|
966
|
-
artifacts: existingArtifacts,
|
|
967
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
968
|
-
reason: 'human-response-blocked',
|
|
969
|
-
capturedEvents: emittedEvents,
|
|
970
|
-
})
|
|
971
|
-
await this.attachCheckpoint(tx, blockedRun, checkpoint)
|
|
972
|
-
return
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
const synced = await this.syncRunGraph({
|
|
976
|
-
tx,
|
|
977
|
-
run,
|
|
978
|
-
spec,
|
|
979
|
-
nodeSpecs,
|
|
980
|
-
nodeRuns,
|
|
981
|
-
artifacts: existingArtifacts,
|
|
982
|
-
emittedBy: params.respondedBy,
|
|
983
|
-
capturedEvents: emittedEvents,
|
|
984
|
-
})
|
|
985
|
-
|
|
986
|
-
await this.emitEvent({
|
|
987
|
-
tx,
|
|
988
|
-
run: synced.run,
|
|
989
|
-
spec,
|
|
990
|
-
nodeId: nextNodeRun.nodeId,
|
|
991
|
-
attemptId: attempt.id,
|
|
992
|
-
approvalId: approval.id,
|
|
993
|
-
eventType: 'approval-resolved',
|
|
994
|
-
fromStatus: run.status,
|
|
995
|
-
toStatus: synced.run.status,
|
|
996
|
-
message: `Human response for node "${nodeSpec.label}" accepted.`,
|
|
997
|
-
detail: { approvalStatus: approvalStatus },
|
|
998
|
-
emittedBy: params.respondedBy,
|
|
999
|
-
capturedEvents: emittedEvents,
|
|
1000
|
-
})
|
|
1001
|
-
|
|
1002
|
-
const checkpoint = await this.saveCheckpoint({
|
|
1003
|
-
tx,
|
|
1004
|
-
run: synced.run,
|
|
1005
|
-
spec,
|
|
1006
|
-
nodeRuns: synced.nodeRuns,
|
|
1007
|
-
artifacts: synced.artifacts,
|
|
1008
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
1009
|
-
reason: 'human-response-complete',
|
|
1010
|
-
capturedEvents: emittedEvents,
|
|
1011
|
-
})
|
|
1012
|
-
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
1013
|
-
})
|
|
1014
|
-
|
|
1015
|
-
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
1016
|
-
|
|
1017
|
-
return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
|
|
1018
|
-
includeEvents: true,
|
|
1019
|
-
includeArtifacts: true,
|
|
1020
|
-
includeApprovals: true,
|
|
1021
|
-
includeCheckpoints: true,
|
|
1022
|
-
includeValidationIssues: true,
|
|
1023
|
-
})
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
async resumeRun(params: {
|
|
1027
|
-
threadId: RecordIdInput
|
|
1028
|
-
runId: string
|
|
1029
|
-
emittedBy: string
|
|
1030
|
-
}): Promise<ExecutionPlanToolResultData> {
|
|
1031
|
-
const run = await planRunService.getRunById(params.runId)
|
|
1032
|
-
|
|
1033
|
-
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
1034
|
-
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
1035
|
-
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
1036
|
-
const artifacts = await planRunService.listArtifacts(run.id)
|
|
1037
|
-
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
1038
|
-
const emittedEvents: PlanEventRecord[] = []
|
|
1039
|
-
|
|
1040
|
-
await databaseService.withTransaction(async (tx) => {
|
|
1041
|
-
let currentNodeRuns = [...nodeRuns]
|
|
1042
|
-
for (const nodeRun of currentNodeRuns.filter((candidate) => candidate.status === 'running')) {
|
|
1043
|
-
const resetNodeRun = PlanNodeRunSchema.parse(
|
|
1044
|
-
await tx
|
|
1045
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1046
|
-
.content(
|
|
1047
|
-
toNodeRunData(nodeRun, {
|
|
1048
|
-
status: 'ready',
|
|
1049
|
-
readyAt: new Date(),
|
|
1050
|
-
startedAt: nodeRun.startedAt ?? new Date(),
|
|
1051
|
-
}),
|
|
1052
|
-
)
|
|
1053
|
-
.output('after'),
|
|
1054
|
-
)
|
|
1055
|
-
currentNodeRuns = currentNodeRuns.map((candidate) =>
|
|
1056
|
-
candidate.nodeId === resetNodeRun.nodeId ? resetNodeRun : candidate,
|
|
1057
|
-
)
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const resetRun = await this.replaceRun(tx, run, {
|
|
1061
|
-
status: run.status === 'awaiting-human' ? 'awaiting-human' : 'running',
|
|
1062
|
-
currentNodeId: run.status === 'awaiting-human' ? (run.currentNodeId ?? null) : null,
|
|
1063
|
-
waitingNodeId: run.status === 'awaiting-human' ? (run.waitingNodeId ?? null) : null,
|
|
1064
|
-
readyNodeIds: currentNodeRuns
|
|
1065
|
-
.filter((candidate) => candidate.status === 'ready')
|
|
1066
|
-
.map((candidate) => candidate.nodeId),
|
|
1067
|
-
})
|
|
1068
|
-
|
|
1069
|
-
await this.emitEvent({
|
|
1070
|
-
tx,
|
|
1071
|
-
run: resetRun,
|
|
1072
|
-
spec,
|
|
1073
|
-
eventType: 'run-resumed',
|
|
1074
|
-
fromStatus: run.status,
|
|
1075
|
-
toStatus: resetRun.status,
|
|
1076
|
-
message: `Run "${spec.title}" resumed from the latest checkpoint.`,
|
|
1077
|
-
detail: latestCheckpoint ? { checkpointId: recordIdToString(latestCheckpoint.id, TABLES.PLAN_CHECKPOINT) } : {},
|
|
1078
|
-
emittedBy: params.emittedBy,
|
|
1079
|
-
capturedEvents: emittedEvents,
|
|
1080
|
-
})
|
|
1081
|
-
|
|
1082
|
-
const synced =
|
|
1083
|
-
resetRun.status === 'awaiting-human'
|
|
1084
|
-
? { run: resetRun, nodeRuns: currentNodeRuns, artifacts }
|
|
1085
|
-
: await this.syncRunGraph({
|
|
1086
|
-
tx,
|
|
1087
|
-
run: resetRun,
|
|
1088
|
-
spec,
|
|
1089
|
-
nodeSpecs,
|
|
1090
|
-
nodeRuns: currentNodeRuns,
|
|
1091
|
-
artifacts,
|
|
1092
|
-
emittedBy: params.emittedBy,
|
|
1093
|
-
capturedEvents: emittedEvents,
|
|
1094
|
-
})
|
|
1095
|
-
|
|
1096
|
-
const checkpoint = await this.saveCheckpoint({
|
|
1097
|
-
tx,
|
|
1098
|
-
run: synced.run,
|
|
1099
|
-
spec,
|
|
1100
|
-
nodeRuns: synced.nodeRuns,
|
|
1101
|
-
artifacts: synced.artifacts,
|
|
1102
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
1103
|
-
reason: 'run-resumed',
|
|
1104
|
-
capturedEvents: emittedEvents,
|
|
1105
|
-
})
|
|
1106
|
-
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
1107
|
-
})
|
|
1108
|
-
|
|
1109
|
-
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
1110
|
-
|
|
1111
|
-
const snapshot = await planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
|
|
1112
|
-
includeEvents: true,
|
|
1113
|
-
includeArtifacts: true,
|
|
1114
|
-
includeApprovals: true,
|
|
1115
|
-
includeCheckpoints: true,
|
|
1116
|
-
includeValidationIssues: true,
|
|
1117
|
-
})
|
|
1118
|
-
|
|
1119
|
-
return buildExecutionPlanToolResult({
|
|
1120
|
-
action: 'run-resumed',
|
|
1121
|
-
plan: snapshot,
|
|
1122
|
-
message: `Resumed execution run "${snapshot.title}".`,
|
|
1123
|
-
})
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
async transitionNodeToRunning(params: { runId: string; nodeId: string }): Promise<void> {
|
|
1127
|
-
const run = await planRunService.getRunById(params.runId)
|
|
1128
|
-
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
1129
|
-
if (nodeRun.status !== 'ready') return
|
|
1130
|
-
|
|
1131
|
-
await databaseService.withTransaction(async (tx) => {
|
|
1132
|
-
const runningNodeRun = PlanNodeRunSchema.parse(
|
|
1133
|
-
await tx
|
|
1134
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1135
|
-
.content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? new Date() }))
|
|
1136
|
-
.output('after'),
|
|
1137
|
-
)
|
|
1138
|
-
|
|
1139
|
-
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
1140
|
-
await this.replaceRun(tx, run, {
|
|
1141
|
-
status: 'running',
|
|
1142
|
-
currentNodeId: runningNodeRun.nodeId,
|
|
1143
|
-
waitingNodeId: null,
|
|
1144
|
-
readyNodeIds: nodeRuns
|
|
1145
|
-
.filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
|
|
1146
|
-
.map((candidate) => candidate.nodeId),
|
|
1147
|
-
})
|
|
1148
|
-
})
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
async blockNodeOnDispatchFailure(params: {
|
|
1152
|
-
threadId: RecordIdInput
|
|
1153
|
-
runId: string
|
|
1154
|
-
nodeId: string
|
|
1155
|
-
emittedBy: string
|
|
1156
|
-
message: string
|
|
1157
|
-
failureClass: PlanFailureClass
|
|
1158
|
-
}): Promise<SerializableExecutionPlan> {
|
|
1159
|
-
const run = await planRunService.getRunById(params.runId)
|
|
1160
|
-
|
|
1161
|
-
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
1162
|
-
const nodeSpec = await planRunService.getNodeSpecByNodeId(spec.id, params.nodeId)
|
|
1163
|
-
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
1164
|
-
const artifacts = await planRunService.listArtifacts(run.id)
|
|
1165
|
-
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
1166
|
-
const emittedEvents: PlanEventRecord[] = []
|
|
1167
|
-
|
|
1168
|
-
await databaseService.withTransaction(async (tx) => {
|
|
1169
|
-
const blockedNodeRun =
|
|
1170
|
-
nodeRun.status === 'blocked'
|
|
1171
|
-
? nodeRun
|
|
1172
|
-
: PlanNodeRunSchema.parse(
|
|
1173
|
-
await tx
|
|
1174
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1175
|
-
.content(
|
|
1176
|
-
toNodeRunData(nodeRun, {
|
|
1177
|
-
status: 'blocked',
|
|
1178
|
-
blockedReason: params.message,
|
|
1179
|
-
failureClass: params.failureClass,
|
|
1180
|
-
}),
|
|
1181
|
-
)
|
|
1182
|
-
.output('after'),
|
|
1183
|
-
)
|
|
1184
|
-
|
|
1185
|
-
const blockedRun = await this.replaceRun(tx, run, {
|
|
1186
|
-
status: 'blocked',
|
|
1187
|
-
currentNodeId: blockedNodeRun.nodeId,
|
|
1188
|
-
waitingNodeId: null,
|
|
1189
|
-
readyNodeIds: [],
|
|
1190
|
-
failureCount: run.failureCount + 1,
|
|
1191
|
-
})
|
|
1192
|
-
|
|
1193
|
-
await this.emitEvent({
|
|
1194
|
-
tx,
|
|
1195
|
-
run: blockedRun,
|
|
1196
|
-
spec,
|
|
1197
|
-
nodeId: blockedNodeRun.nodeId,
|
|
1198
|
-
eventType: 'node-blocked',
|
|
1199
|
-
fromStatus: nodeRun.status,
|
|
1200
|
-
toStatus: blockedNodeRun.status,
|
|
1201
|
-
message: `Node "${nodeSpec.label}" failed during owner dispatch.`,
|
|
1202
|
-
detail: { failureClass: params.failureClass, phase: 'dispatch', error: params.message },
|
|
1203
|
-
emittedBy: params.emittedBy,
|
|
1204
|
-
capturedEvents: emittedEvents,
|
|
1205
|
-
})
|
|
1206
|
-
|
|
1207
|
-
const checkpoint = await this.saveCheckpoint({
|
|
1208
|
-
tx,
|
|
1209
|
-
run: blockedRun,
|
|
1210
|
-
spec,
|
|
1211
|
-
nodeRuns: (await planRunService.listNodeRuns(run.id)).map((candidate) =>
|
|
1212
|
-
candidate.nodeId === blockedNodeRun.nodeId ? blockedNodeRun : candidate,
|
|
1213
|
-
),
|
|
1214
|
-
artifacts,
|
|
1215
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
1216
|
-
reason: 'owner-dispatch-failed',
|
|
1217
|
-
capturedEvents: emittedEvents,
|
|
1218
|
-
})
|
|
1219
|
-
await this.attachCheckpoint(tx, blockedRun, checkpoint)
|
|
1220
|
-
})
|
|
1221
|
-
|
|
1222
|
-
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
1223
|
-
|
|
1224
|
-
return planRunService.toSerializablePlan(await planRunService.getRunById(run.id), {
|
|
1225
|
-
includeEvents: true,
|
|
1226
|
-
includeArtifacts: true,
|
|
1227
|
-
includeApprovals: true,
|
|
1228
|
-
includeCheckpoints: true,
|
|
1229
|
-
includeValidationIssues: true,
|
|
1230
|
-
})
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
async promoteDelayedNode(params: { runId: string; nodeId: string; emittedBy: string }): Promise<void> {
|
|
1234
|
-
const run = await planRunService.getRunById(params.runId)
|
|
1235
|
-
if (run.status === 'completed' || run.status === 'failed' || run.status === 'aborted') {
|
|
1236
|
-
return // Run is no longer active, skip promotion
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
const spec = await planRunService.getPlanSpecById(run.planSpecId)
|
|
1240
|
-
const nodeSpecs = await planRunService.listNodeSpecs(spec.id)
|
|
1241
|
-
const nodeRun = await planRunService.getNodeRunByNodeId(run.id, params.nodeId)
|
|
1242
|
-
|
|
1243
|
-
// Only promote if still in scheduled state (delay hasn't been superseded)
|
|
1244
|
-
if (nodeRun.status !== 'scheduled') return
|
|
1245
|
-
|
|
1246
|
-
const nodeRuns = await planRunService.listNodeRuns(run.id)
|
|
1247
|
-
const artifacts = await planRunService.listArtifacts(run.id)
|
|
1248
|
-
const latestCheckpoint = await planRunService.getLatestCheckpoint(run.id)
|
|
1249
|
-
const emittedEvents: PlanEventRecord[] = []
|
|
1250
|
-
|
|
1251
|
-
await databaseService.withTransaction(async (tx) => {
|
|
1252
|
-
const readyNodeRun = PlanNodeRunSchema.parse(
|
|
1253
|
-
await tx
|
|
1254
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1255
|
-
.content(toNodeRunData(nodeRun, { status: 'ready', readyAt: new Date() }))
|
|
1256
|
-
.output('after'),
|
|
1257
|
-
)
|
|
1258
|
-
|
|
1259
|
-
const updatedNodeRuns = nodeRuns.map((candidate) =>
|
|
1260
|
-
candidate.nodeId === readyNodeRun.nodeId ? readyNodeRun : candidate,
|
|
1261
|
-
)
|
|
1262
|
-
|
|
1263
|
-
const nodeSpec = nodeSpecs.find((s) => s.nodeId === params.nodeId)
|
|
1264
|
-
await this.emitEvent({
|
|
1265
|
-
tx,
|
|
1266
|
-
run,
|
|
1267
|
-
spec,
|
|
1268
|
-
nodeId: readyNodeRun.nodeId,
|
|
1269
|
-
eventType: 'node-ready',
|
|
1270
|
-
fromStatus: nodeRun.status,
|
|
1271
|
-
toStatus: readyNodeRun.status,
|
|
1272
|
-
message: `Node "${nodeSpec?.label ?? params.nodeId}" promoted to ready after delay.`,
|
|
1273
|
-
emittedBy: params.emittedBy,
|
|
1274
|
-
capturedEvents: emittedEvents,
|
|
1275
|
-
})
|
|
1276
|
-
|
|
1277
|
-
const synced = await this.syncRunGraph({
|
|
1278
|
-
tx,
|
|
1279
|
-
run,
|
|
1280
|
-
spec,
|
|
1281
|
-
nodeSpecs,
|
|
1282
|
-
nodeRuns: updatedNodeRuns,
|
|
1283
|
-
artifacts,
|
|
1284
|
-
emittedBy: params.emittedBy,
|
|
1285
|
-
capturedEvents: emittedEvents,
|
|
1286
|
-
})
|
|
1287
|
-
|
|
1288
|
-
const checkpoint = await this.saveCheckpoint({
|
|
1289
|
-
tx,
|
|
1290
|
-
run: synced.run,
|
|
1291
|
-
spec,
|
|
1292
|
-
nodeRuns: synced.nodeRuns,
|
|
1293
|
-
artifacts: synced.artifacts,
|
|
1294
|
-
sequence: (latestCheckpoint?.sequence ?? 0) + 1,
|
|
1295
|
-
reason: 'delayed-node-promoted',
|
|
1296
|
-
capturedEvents: emittedEvents,
|
|
1297
|
-
})
|
|
1298
|
-
await this.attachCheckpoint(tx, synced.run, checkpoint)
|
|
1299
|
-
})
|
|
1300
|
-
|
|
1301
|
-
await planEventDeliveryService.dispatchEvents(emittedEvents)
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
async syncRunGraph(params: {
|
|
1305
|
-
tx: DatabaseTransaction
|
|
1306
|
-
run: PlanRunRecord
|
|
1307
|
-
spec: PlanSpecRecord
|
|
1308
|
-
nodeSpecs: PlanNodeSpecRecord[]
|
|
1309
|
-
nodeRuns: PlanNodeRunRecord[]
|
|
1310
|
-
artifacts: Array<{
|
|
1311
|
-
id: RecordIdInput
|
|
1312
|
-
nodeId: string
|
|
1313
|
-
name: string
|
|
1314
|
-
kind: string
|
|
1315
|
-
pointer: string
|
|
1316
|
-
schemaRef?: string
|
|
1317
|
-
payload?: unknown
|
|
1318
|
-
}>
|
|
1319
|
-
emittedBy: string
|
|
1320
|
-
capturedEvents?: PlanEventRecord[]
|
|
1321
|
-
}): Promise<{
|
|
1322
|
-
run: PlanRunRecord
|
|
1323
|
-
nodeRuns: PlanNodeRunRecord[]
|
|
1324
|
-
artifacts: Array<{
|
|
1325
|
-
id: RecordIdInput
|
|
1326
|
-
nodeId: string
|
|
1327
|
-
name: string
|
|
1328
|
-
kind: string
|
|
1329
|
-
pointer: string
|
|
1330
|
-
schemaRef?: string
|
|
1331
|
-
payload?: unknown
|
|
1332
|
-
}>
|
|
1333
|
-
}> {
|
|
1334
|
-
let currentRun = params.run
|
|
1335
|
-
let currentNodeRuns = [...params.nodeRuns]
|
|
1336
|
-
const currentArtifacts = [...params.artifacts]
|
|
1337
|
-
const sortedNodeSpecs = [...params.nodeSpecs].sort((left, right) => left.position - right.position)
|
|
1338
|
-
|
|
1339
|
-
// Cross-plan dependency check: if spec has block-mode dependencies that are unresolved, block the run
|
|
1340
|
-
if (params.spec.dependencies && params.spec.dependencies.length > 0) {
|
|
1341
|
-
const { unresolved } = await planCoordinationService.resolveDependencies({
|
|
1342
|
-
dependencies: params.spec.dependencies,
|
|
1343
|
-
threadId: recordIdToString(params.spec.threadId, TABLES.THREAD),
|
|
1344
|
-
})
|
|
1345
|
-
if (unresolved.length > 0) {
|
|
1346
|
-
currentRun = await this.replaceRun(params.tx, currentRun, { status: 'blocked', readyNodeIds: [] })
|
|
1347
|
-
await this.emitEvent({
|
|
1348
|
-
tx: params.tx,
|
|
1349
|
-
run: currentRun,
|
|
1350
|
-
spec: params.spec,
|
|
1351
|
-
eventType: 'run-status-changed',
|
|
1352
|
-
fromStatus: params.run.status,
|
|
1353
|
-
toStatus: currentRun.status,
|
|
1354
|
-
message: `Run blocked: unresolved cross-plan dependencies (${unresolved
|
|
1355
|
-
.map((d) => d.sourcePlanSpecId)
|
|
1356
|
-
.join(', ')}).`,
|
|
1357
|
-
emittedBy: params.emittedBy,
|
|
1358
|
-
capturedEvents: params.capturedEvents,
|
|
1359
|
-
})
|
|
1360
|
-
return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
const replaceNodeRun = (nextNodeRun: PlanNodeRunRecord) => {
|
|
1365
|
-
currentNodeRuns = currentNodeRuns.map((candidate) =>
|
|
1366
|
-
candidate.nodeId === nextNodeRun.nodeId ? nextNodeRun : candidate,
|
|
1367
|
-
)
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
const getNodeRunsById = () => new Map(currentNodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun]))
|
|
1371
|
-
const getArtifactsByNodeId = () =>
|
|
1372
|
-
currentArtifacts.reduce((groups, artifact) => {
|
|
1373
|
-
const list = groups.get(artifact.nodeId) ?? []
|
|
1374
|
-
list.push(artifact)
|
|
1375
|
-
groups.set(artifact.nodeId, list)
|
|
1376
|
-
return groups
|
|
1377
|
-
}, new Map<string, typeof currentArtifacts>())
|
|
1378
|
-
|
|
1379
|
-
let changed = true
|
|
1380
|
-
while (changed) {
|
|
1381
|
-
changed = false
|
|
1382
|
-
const nodeRunsById = getNodeRunsById()
|
|
1383
|
-
const artifactsByNodeId = getArtifactsByNodeId()
|
|
1384
|
-
|
|
1385
|
-
for (const nodeSpec of sortedNodeSpecs) {
|
|
1386
|
-
const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
|
|
1387
|
-
if (!nodeRun || nodeRun.status !== 'pending') continue
|
|
1388
|
-
|
|
1389
|
-
const upstreamRuns = nodeSpec.upstreamNodeIds
|
|
1390
|
-
.map((nodeId) => nodeRunsById.get(nodeId))
|
|
1391
|
-
.filter(Boolean) as PlanNodeRunRecord[]
|
|
1392
|
-
if (
|
|
1393
|
-
nodeSpec.upstreamNodeIds.length > 0 &&
|
|
1394
|
-
!upstreamRuns.every((upstreamRun) => isSuccessfulTerminalStatus(upstreamRun.status))
|
|
1395
|
-
) {
|
|
1396
|
-
continue
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
const activeIncomingEdges = params.spec.edges.filter((edge) => {
|
|
1400
|
-
if (edge.target !== nodeSpec.nodeId) return false
|
|
1401
|
-
const sourceRun = nodeRunsById.get(edge.source)
|
|
1402
|
-
if (!sourceRun) return false
|
|
1403
|
-
const context = buildNodeContext({ nodeRun: sourceRun, artifacts: artifactsByNodeId.get(edge.source) ?? [] })
|
|
1404
|
-
return evaluateCondition(edge.when, context)
|
|
1405
|
-
})
|
|
1406
|
-
|
|
1407
|
-
if (nodeSpec.upstreamNodeIds.length > 0 && activeIncomingEdges.length === 0) {
|
|
1408
|
-
const skippedNodeRun = PlanNodeRunSchema.parse(
|
|
1409
|
-
await params.tx
|
|
1410
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1411
|
-
.content(
|
|
1412
|
-
toNodeRunData(nodeRun, {
|
|
1413
|
-
status: 'skipped',
|
|
1414
|
-
completedAt: new Date(),
|
|
1415
|
-
blockedReason: null,
|
|
1416
|
-
failureClass: null,
|
|
1417
|
-
}),
|
|
1418
|
-
)
|
|
1419
|
-
.output('after'),
|
|
1420
|
-
)
|
|
1421
|
-
replaceNodeRun(skippedNodeRun)
|
|
1422
|
-
await this.emitEvent({
|
|
1423
|
-
tx: params.tx,
|
|
1424
|
-
run: currentRun,
|
|
1425
|
-
spec: params.spec,
|
|
1426
|
-
nodeId: skippedNodeRun.nodeId,
|
|
1427
|
-
eventType: 'node-skipped',
|
|
1428
|
-
fromStatus: nodeRun.status,
|
|
1429
|
-
toStatus: skippedNodeRun.status,
|
|
1430
|
-
message: `Node "${nodeSpec.label}" was skipped because no inbound branch was activated.`,
|
|
1431
|
-
emittedBy: params.emittedBy,
|
|
1432
|
-
capturedEvents: params.capturedEvents,
|
|
1433
|
-
})
|
|
1434
|
-
changed = true
|
|
1435
|
-
continue
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
const resolvedInput = this.buildResolvedInput({ spec: params.spec, nodeSpec, nodeRunsById, artifactsByNodeId })
|
|
1439
|
-
|
|
1440
|
-
const nodeSchedule = nodeSpec.schedule
|
|
1441
|
-
const hasNonImmediateSchedule = nodeSchedule && nodeSchedule.type !== 'immediate'
|
|
1442
|
-
|
|
1443
|
-
if (hasNonImmediateSchedule) {
|
|
1444
|
-
const scheduledNodeRun = PlanNodeRunSchema.parse(
|
|
1445
|
-
await params.tx
|
|
1446
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1447
|
-
.content(toNodeRunData(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: new Date() }))
|
|
1448
|
-
.output('after'),
|
|
1449
|
-
)
|
|
1450
|
-
replaceNodeRun(scheduledNodeRun)
|
|
1451
|
-
await planSchedulerService.createSchedule({
|
|
1452
|
-
organizationId: currentRun.organizationId,
|
|
1453
|
-
threadId: currentRun.threadId,
|
|
1454
|
-
planSpecId: params.spec.id,
|
|
1455
|
-
runId: currentRun.id,
|
|
1456
|
-
nodeId: nodeSpec.nodeId,
|
|
1457
|
-
scheduleSpec: nodeSchedule,
|
|
1458
|
-
})
|
|
1459
|
-
await this.emitEvent({
|
|
1460
|
-
tx: params.tx,
|
|
1461
|
-
run: currentRun,
|
|
1462
|
-
spec: params.spec,
|
|
1463
|
-
nodeId: scheduledNodeRun.nodeId,
|
|
1464
|
-
eventType: 'node-scheduled',
|
|
1465
|
-
fromStatus: nodeRun.status,
|
|
1466
|
-
toStatus: scheduledNodeRun.status,
|
|
1467
|
-
message: `Node "${nodeSpec.label}" is scheduled (${nodeSchedule.type}).`,
|
|
1468
|
-
emittedBy: params.emittedBy,
|
|
1469
|
-
capturedEvents: params.capturedEvents,
|
|
1470
|
-
})
|
|
1471
|
-
changed = true
|
|
1472
|
-
} else if (nodeSpec.delayAfterPredecessorMs) {
|
|
1473
|
-
// Event-triggered delay: enqueue a delayed promotion instead of transitioning to ready immediately
|
|
1474
|
-
const { enqueueDelayedNodePromotion } = await import('../queues/delayed-node-promotion.queue')
|
|
1475
|
-
const scheduledNodeRun = PlanNodeRunSchema.parse(
|
|
1476
|
-
await params.tx
|
|
1477
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1478
|
-
.content(toNodeRunData(nodeRun, { status: 'scheduled', resolvedInput, scheduledAt: new Date() }))
|
|
1479
|
-
.output('after'),
|
|
1480
|
-
)
|
|
1481
|
-
replaceNodeRun(scheduledNodeRun)
|
|
1482
|
-
await enqueueDelayedNodePromotion(
|
|
1483
|
-
{
|
|
1484
|
-
runId: recordIdToString(currentRun.id, TABLES.PLAN_RUN),
|
|
1485
|
-
nodeId: nodeSpec.nodeId,
|
|
1486
|
-
emittedBy: params.emittedBy,
|
|
1487
|
-
},
|
|
1488
|
-
nodeSpec.delayAfterPredecessorMs,
|
|
1489
|
-
)
|
|
1490
|
-
await this.emitEvent({
|
|
1491
|
-
tx: params.tx,
|
|
1492
|
-
run: currentRun,
|
|
1493
|
-
spec: params.spec,
|
|
1494
|
-
nodeId: scheduledNodeRun.nodeId,
|
|
1495
|
-
eventType: 'node-scheduled',
|
|
1496
|
-
fromStatus: nodeRun.status,
|
|
1497
|
-
toStatus: scheduledNodeRun.status,
|
|
1498
|
-
message: `Node "${nodeSpec.label}" is delayed by ${nodeSpec.delayAfterPredecessorMs}ms after predecessor.`,
|
|
1499
|
-
emittedBy: params.emittedBy,
|
|
1500
|
-
capturedEvents: params.capturedEvents,
|
|
1501
|
-
})
|
|
1502
|
-
changed = true
|
|
1503
|
-
} else {
|
|
1504
|
-
const readyNodeRun = PlanNodeRunSchema.parse(
|
|
1505
|
-
await params.tx
|
|
1506
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1507
|
-
.content(toNodeRunData(nodeRun, { status: 'ready', resolvedInput, readyAt: new Date() }))
|
|
1508
|
-
.output('after'),
|
|
1509
|
-
)
|
|
1510
|
-
replaceNodeRun(readyNodeRun)
|
|
1511
|
-
await this.emitEvent({
|
|
1512
|
-
tx: params.tx,
|
|
1513
|
-
run: currentRun,
|
|
1514
|
-
spec: params.spec,
|
|
1515
|
-
nodeId: readyNodeRun.nodeId,
|
|
1516
|
-
eventType: 'node-ready',
|
|
1517
|
-
fromStatus: nodeRun.status,
|
|
1518
|
-
toStatus: readyNodeRun.status,
|
|
1519
|
-
message: `Node "${nodeSpec.label}" is ready to execute.`,
|
|
1520
|
-
emittedBy: params.emittedBy,
|
|
1521
|
-
capturedEvents: params.capturedEvents,
|
|
1522
|
-
})
|
|
1523
|
-
changed = true
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
const readyStructuralNodes = sortedNodeSpecs.filter((nodeSpec) => {
|
|
1528
|
-
const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
|
|
1529
|
-
return nodeRun?.status === 'ready' && isStructuralNodeType(nodeSpec.type)
|
|
1530
|
-
})
|
|
1531
|
-
|
|
1532
|
-
for (const nodeSpec of readyStructuralNodes) {
|
|
1533
|
-
const nodeRun = getNodeRunsById().get(nodeSpec.nodeId)
|
|
1534
|
-
if (!nodeRun) continue
|
|
1535
|
-
|
|
1536
|
-
const completedNodeRun = PlanNodeRunSchema.parse(
|
|
1537
|
-
await params.tx
|
|
1538
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1539
|
-
.content(
|
|
1540
|
-
toNodeRunData(nodeRun, {
|
|
1541
|
-
status: 'completed',
|
|
1542
|
-
startedAt: nodeRun.startedAt ?? new Date(),
|
|
1543
|
-
completedAt: new Date(),
|
|
1544
|
-
}),
|
|
1545
|
-
)
|
|
1546
|
-
.output('after'),
|
|
1547
|
-
)
|
|
1548
|
-
replaceNodeRun(completedNodeRun)
|
|
1549
|
-
await this.emitEvent({
|
|
1550
|
-
tx: params.tx,
|
|
1551
|
-
run: currentRun,
|
|
1552
|
-
spec: params.spec,
|
|
1553
|
-
nodeId: completedNodeRun.nodeId,
|
|
1554
|
-
eventType: 'node-auto-completed',
|
|
1555
|
-
fromStatus: nodeRun.status,
|
|
1556
|
-
toStatus: completedNodeRun.status,
|
|
1557
|
-
message: `Structural node "${nodeSpec.label}" auto-completed.`,
|
|
1558
|
-
emittedBy: params.emittedBy,
|
|
1559
|
-
capturedEvents: params.capturedEvents,
|
|
1560
|
-
})
|
|
1561
|
-
changed = true
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
const nodeRunsById = getNodeRunsById()
|
|
1566
|
-
const activeRunningNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'running')
|
|
1567
|
-
const activeHumanNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'awaiting-human')
|
|
1568
|
-
const activeMonitoringNode = currentNodeRuns.find((nodeRun) => nodeRun.status === 'monitoring')
|
|
1569
|
-
const hasScheduledOrMonitoring = currentNodeRuns.some(
|
|
1570
|
-
(nodeRun) => nodeRun.status === 'scheduled' || nodeRun.status === 'monitoring',
|
|
1571
|
-
)
|
|
1572
|
-
|
|
1573
|
-
if (!activeRunningNode && !activeHumanNode && !activeMonitoringNode) {
|
|
1574
|
-
const nextHumanNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
|
|
1575
|
-
const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
|
|
1576
|
-
return nodeRun?.status === 'ready' && isHumanNodeType(nodeSpec.type)
|
|
1577
|
-
})
|
|
1578
|
-
|
|
1579
|
-
if (nextHumanNodeSpec) {
|
|
1580
|
-
const nodeRun = nodeRunsById.get(nextHumanNodeSpec.nodeId)
|
|
1581
|
-
if (!nodeRun) {
|
|
1582
|
-
throw new Error(`Expected ready node run for "${nextHumanNodeSpec.nodeId}".`)
|
|
1583
|
-
}
|
|
1584
|
-
const awaitingHumanNodeRun = PlanNodeRunSchema.parse(
|
|
1585
|
-
await params.tx
|
|
1586
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1587
|
-
.content(toNodeRunData(nodeRun, { status: 'awaiting-human', startedAt: nodeRun.startedAt ?? new Date() }))
|
|
1588
|
-
.output('after'),
|
|
1589
|
-
)
|
|
1590
|
-
replaceNodeRun(awaitingHumanNodeRun)
|
|
1591
|
-
|
|
1592
|
-
const approval = await planApprovalService.createPendingApproval({
|
|
1593
|
-
tx: params.tx,
|
|
1594
|
-
runId: currentRun.id,
|
|
1595
|
-
nodeRunId: awaitingHumanNodeRun.id,
|
|
1596
|
-
nodeId: awaitingHumanNodeRun.nodeId,
|
|
1597
|
-
requestedBy: params.emittedBy,
|
|
1598
|
-
presented: {
|
|
1599
|
-
nodeId: nextHumanNodeSpec.nodeId,
|
|
1600
|
-
label: nextHumanNodeSpec.label,
|
|
1601
|
-
objective: nextHumanNodeSpec.objective,
|
|
1602
|
-
instructions: nextHumanNodeSpec.instructions,
|
|
1603
|
-
deliverables: nextHumanNodeSpec.deliverables,
|
|
1604
|
-
successCriteria: nextHumanNodeSpec.successCriteria,
|
|
1605
|
-
resolvedInput: awaitingHumanNodeRun.resolvedInput ?? {},
|
|
1606
|
-
},
|
|
1607
|
-
})
|
|
1608
|
-
|
|
1609
|
-
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1610
|
-
status: 'awaiting-human',
|
|
1611
|
-
currentNodeId: awaitingHumanNodeRun.nodeId,
|
|
1612
|
-
waitingNodeId: awaitingHumanNodeRun.nodeId,
|
|
1613
|
-
readyNodeIds: currentNodeRuns
|
|
1614
|
-
.filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== awaitingHumanNodeRun.nodeId)
|
|
1615
|
-
.map((candidate) => candidate.nodeId),
|
|
1616
|
-
})
|
|
1617
|
-
|
|
1618
|
-
await this.emitEvent({
|
|
1619
|
-
tx: params.tx,
|
|
1620
|
-
run: currentRun,
|
|
1621
|
-
spec: params.spec,
|
|
1622
|
-
nodeId: awaitingHumanNodeRun.nodeId,
|
|
1623
|
-
approvalId: approval.id,
|
|
1624
|
-
eventType: 'approval-requested',
|
|
1625
|
-
fromStatus: params.run.status,
|
|
1626
|
-
toStatus: currentRun.status,
|
|
1627
|
-
message: `Node "${nextHumanNodeSpec.label}" is awaiting human input.`,
|
|
1628
|
-
emittedBy: params.emittedBy,
|
|
1629
|
-
capturedEvents: params.capturedEvents,
|
|
1630
|
-
})
|
|
1631
|
-
} else {
|
|
1632
|
-
const nextActionNodeSpec = sortedNodeSpecs.find((nodeSpec) => {
|
|
1633
|
-
const nodeRun = nodeRunsById.get(nodeSpec.nodeId)
|
|
1634
|
-
return nodeRun?.status === 'ready' && !isStructuralNodeType(nodeSpec.type)
|
|
1635
|
-
})
|
|
1636
|
-
|
|
1637
|
-
if (nextActionNodeSpec) {
|
|
1638
|
-
const nodeRun = nodeRunsById.get(nextActionNodeSpec.nodeId)
|
|
1639
|
-
if (!nodeRun) {
|
|
1640
|
-
throw new Error(`Expected ready node run for "${nextActionNodeSpec.nodeId}".`)
|
|
1641
|
-
}
|
|
1642
|
-
const runningNodeRun = PlanNodeRunSchema.parse(
|
|
1643
|
-
await params.tx
|
|
1644
|
-
.update(ensureRecordId(nodeRun.id, TABLES.PLAN_NODE_RUN))
|
|
1645
|
-
.content(toNodeRunData(nodeRun, { status: 'running', startedAt: nodeRun.startedAt ?? new Date() }))
|
|
1646
|
-
.output('after'),
|
|
1647
|
-
)
|
|
1648
|
-
replaceNodeRun(runningNodeRun)
|
|
1649
|
-
|
|
1650
|
-
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1651
|
-
status: 'running',
|
|
1652
|
-
currentNodeId: runningNodeRun.nodeId,
|
|
1653
|
-
waitingNodeId: null,
|
|
1654
|
-
readyNodeIds: currentNodeRuns
|
|
1655
|
-
.filter((candidate) => candidate.status === 'ready' && candidate.nodeId !== runningNodeRun.nodeId)
|
|
1656
|
-
.map((candidate) => candidate.nodeId),
|
|
1657
|
-
})
|
|
1658
|
-
|
|
1659
|
-
await this.emitEvent({
|
|
1660
|
-
tx: params.tx,
|
|
1661
|
-
run: currentRun,
|
|
1662
|
-
spec: params.spec,
|
|
1663
|
-
nodeId: runningNodeRun.nodeId,
|
|
1664
|
-
eventType: 'node-running',
|
|
1665
|
-
fromStatus: nodeRun.status,
|
|
1666
|
-
toStatus: runningNodeRun.status,
|
|
1667
|
-
message: `Node "${nextActionNodeSpec.label}" is now running.`,
|
|
1668
|
-
emittedBy: params.emittedBy,
|
|
1669
|
-
capturedEvents: params.capturedEvents,
|
|
1670
|
-
})
|
|
1671
|
-
await this.emitEvent({
|
|
1672
|
-
tx: params.tx,
|
|
1673
|
-
run: currentRun,
|
|
1674
|
-
spec: params.spec,
|
|
1675
|
-
nodeId: runningNodeRun.nodeId,
|
|
1676
|
-
eventType: 'ownership-transition',
|
|
1677
|
-
fromStatus: params.run.currentNodeId ?? undefined,
|
|
1678
|
-
toStatus: runningNodeRun.nodeId,
|
|
1679
|
-
message: `Execution ownership transitioned to "${nextActionNodeSpec.label}".`,
|
|
1680
|
-
detail: { owner: nextActionNodeSpec.owner },
|
|
1681
|
-
emittedBy: params.emittedBy,
|
|
1682
|
-
capturedEvents: params.capturedEvents,
|
|
1683
|
-
})
|
|
1684
|
-
} else if (currentNodeRuns.every((nodeRun) => isSuccessfulTerminalStatus(nodeRun.status))) {
|
|
1685
|
-
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1686
|
-
status: 'completed',
|
|
1687
|
-
currentNodeId: null,
|
|
1688
|
-
waitingNodeId: null,
|
|
1689
|
-
readyNodeIds: [],
|
|
1690
|
-
completedAt: new Date(),
|
|
1691
|
-
})
|
|
1692
|
-
|
|
1693
|
-
await this.emitEvent({
|
|
1694
|
-
tx: params.tx,
|
|
1695
|
-
run: currentRun,
|
|
1696
|
-
spec: params.spec,
|
|
1697
|
-
eventType: 'run-status-changed',
|
|
1698
|
-
fromStatus: params.run.status,
|
|
1699
|
-
toStatus: currentRun.status,
|
|
1700
|
-
message: `Run "${params.spec.title}" completed.`,
|
|
1701
|
-
emittedBy: params.emittedBy,
|
|
1702
|
-
capturedEvents: params.capturedEvents,
|
|
1703
|
-
})
|
|
1704
|
-
} else if (hasScheduledOrMonitoring) {
|
|
1705
|
-
// Nodes are waiting on schedules/monitors — run stays active
|
|
1706
|
-
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1707
|
-
status: 'running',
|
|
1708
|
-
currentNodeId: null,
|
|
1709
|
-
waitingNodeId: null,
|
|
1710
|
-
readyNodeIds: currentNodeRuns
|
|
1711
|
-
.filter((candidate) => candidate.status === 'ready')
|
|
1712
|
-
.map((candidate) => candidate.nodeId),
|
|
1713
|
-
})
|
|
1714
|
-
} else {
|
|
1715
|
-
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1716
|
-
status: 'blocked',
|
|
1717
|
-
currentNodeId: null,
|
|
1718
|
-
waitingNodeId: null,
|
|
1719
|
-
readyNodeIds: currentNodeRuns
|
|
1720
|
-
.filter((candidate) => candidate.status === 'ready')
|
|
1721
|
-
.map((candidate) => candidate.nodeId),
|
|
1722
|
-
})
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
} else {
|
|
1726
|
-
currentRun = await this.replaceRun(params.tx, currentRun, {
|
|
1727
|
-
status: activeHumanNode ? 'awaiting-human' : 'running',
|
|
1728
|
-
currentNodeId: activeHumanNode?.nodeId ?? activeMonitoringNode?.nodeId ?? activeRunningNode?.nodeId ?? null,
|
|
1729
|
-
waitingNodeId: activeHumanNode?.nodeId ?? null,
|
|
1730
|
-
readyNodeIds: currentNodeRuns
|
|
1731
|
-
.filter((candidate) => candidate.status === 'ready')
|
|
1732
|
-
.map((candidate) => candidate.nodeId),
|
|
1733
|
-
})
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
return { run: currentRun, nodeRuns: currentNodeRuns, artifacts: currentArtifacts }
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
private resolveFailureAction(nodeSpec: PlanNodeSpecRecord, failureClass: PlanFailureClass | null): PlanFailureAction {
|
|
1740
|
-
if (!failureClass) return 'abort'
|
|
1741
|
-
|
|
1742
|
-
const matchedRule = nodeSpec.failurePolicy.find((rule) => rule.on === failureClass)
|
|
1743
|
-
return matchedRule?.action ?? 'abort'
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
private buildResolvedInput(params: {
|
|
1747
|
-
spec: PlanSpecRecord
|
|
1748
|
-
nodeSpec: PlanNodeSpecRecord
|
|
1749
|
-
nodeRunsById: Map<string, PlanNodeRunRecord>
|
|
1750
|
-
artifactsByNodeId: Map<
|
|
1751
|
-
string,
|
|
1752
|
-
Array<{ nodeId: string; name: string; kind: string; pointer: string; schemaRef?: string; payload?: unknown }>
|
|
1753
|
-
>
|
|
1754
|
-
}) {
|
|
1755
|
-
const resolvedInput: Record<string, unknown> = {}
|
|
1756
|
-
|
|
1757
|
-
for (const edge of params.spec.edges.filter((candidate) => candidate.target === params.nodeSpec.nodeId)) {
|
|
1758
|
-
const sourceRun = params.nodeRunsById.get(edge.source)
|
|
1759
|
-
if (!sourceRun || !isSuccessfulTerminalStatus(sourceRun.status)) continue
|
|
1760
|
-
|
|
1761
|
-
const context = buildNodeContext({
|
|
1762
|
-
nodeRun: sourceRun,
|
|
1763
|
-
artifacts: params.artifactsByNodeId.get(edge.source) ?? [],
|
|
1764
|
-
})
|
|
1765
|
-
if (!evaluateCondition(edge.when, context)) continue
|
|
1766
|
-
|
|
1767
|
-
for (const [targetPath, sourcePath] of Object.entries(edge.map)) {
|
|
1768
|
-
const value = readPathValue(context, sourcePath)
|
|
1769
|
-
if (value !== undefined) {
|
|
1770
|
-
setPathValue(resolvedInput, targetPath, value)
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
return resolvedInput
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
private async createAttempt(params: {
|
|
1779
|
-
tx: DatabaseTransaction
|
|
1780
|
-
run: PlanRunRecord
|
|
1781
|
-
nodeRun: PlanNodeRunRecord
|
|
1782
|
-
emittedBy: string
|
|
1783
|
-
result: PlanNodeResultSubmission
|
|
1784
|
-
status: 'completed' | 'failed'
|
|
1785
|
-
failureClass: PlanFailureClass | null
|
|
1786
|
-
}) {
|
|
1787
|
-
const attemptId = new RecordId(TABLES.PLAN_NODE_ATTEMPT, Bun.randomUUIDv7())
|
|
1788
|
-
return PlanNodeAttemptSchema.parse(
|
|
1789
|
-
await params.tx
|
|
1790
|
-
.create(attemptId)
|
|
1791
|
-
.content({
|
|
1792
|
-
runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
|
|
1793
|
-
nodeRunId: ensureRecordId(params.nodeRun.id, TABLES.PLAN_NODE_RUN),
|
|
1794
|
-
nodeId: params.nodeRun.nodeId,
|
|
1795
|
-
emittedBy: params.emittedBy,
|
|
1796
|
-
status: params.status,
|
|
1797
|
-
...(params.result.structuredOutput ? { structuredOutput: params.result.structuredOutput } : {}),
|
|
1798
|
-
...(params.result.notes ? { notes: params.result.notes } : {}),
|
|
1799
|
-
validationIssueIds: [],
|
|
1800
|
-
...(params.failureClass ? { failureClass: params.failureClass } : {}),
|
|
1801
|
-
})
|
|
1802
|
-
.output('after'),
|
|
1803
|
-
)
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
private async persistValidationIssues(params: {
|
|
1807
|
-
tx: DatabaseTransaction
|
|
1808
|
-
run: PlanRunRecord
|
|
1809
|
-
spec: PlanSpecRecord
|
|
1810
|
-
attemptId: RecordIdInput
|
|
1811
|
-
nodeId: string
|
|
1812
|
-
issues: PlanValidationIssueInput[]
|
|
1813
|
-
}): Promise<PlanValidationIssueRecord[]> {
|
|
1814
|
-
const records: PlanValidationIssueRecord[] = []
|
|
1815
|
-
|
|
1816
|
-
for (const issue of params.issues) {
|
|
1817
|
-
const issueId = new RecordId(TABLES.PLAN_VALIDATION_ISSUE, Bun.randomUUIDv7())
|
|
1818
|
-
const created = await params.tx
|
|
1819
|
-
.create(issueId)
|
|
1820
|
-
.content({
|
|
1821
|
-
planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
|
|
1822
|
-
runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
|
|
1823
|
-
nodeId: issue.nodeId ?? params.nodeId,
|
|
1824
|
-
attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT),
|
|
1825
|
-
severity: issue.severity,
|
|
1826
|
-
code: issue.code,
|
|
1827
|
-
message: issue.message,
|
|
1828
|
-
...(issue.detail ? { detail: issue.detail } : {}),
|
|
1829
|
-
})
|
|
1830
|
-
.output('after')
|
|
1831
|
-
|
|
1832
|
-
records.push(PlanValidationIssueSchema.parse(created))
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
return records
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
private async emitEvent(params: {
|
|
1839
|
-
tx: DatabaseTransaction
|
|
1840
|
-
run: PlanRunRecord
|
|
1841
|
-
spec: PlanSpecRecord
|
|
1842
|
-
eventType: PlanEventType
|
|
1843
|
-
message: string
|
|
1844
|
-
emittedBy: string
|
|
1845
|
-
nodeId?: string
|
|
1846
|
-
attemptId?: RecordIdInput
|
|
1847
|
-
approvalId?: RecordIdInput
|
|
1848
|
-
fromStatus?: string
|
|
1849
|
-
toStatus?: string
|
|
1850
|
-
detail?: Record<string, unknown>
|
|
1851
|
-
capturedEvents?: PlanEventRecord[]
|
|
1852
|
-
}): Promise<PlanEventRecord> {
|
|
1853
|
-
const eventId = new RecordId(TABLES.PLAN_EVENT, Bun.randomUUIDv7())
|
|
1854
|
-
const created = await params.tx
|
|
1855
|
-
.create(eventId)
|
|
1856
|
-
.content({
|
|
1857
|
-
planSpecId: ensureRecordId(params.spec.id, TABLES.PLAN_SPEC),
|
|
1858
|
-
runId: ensureRecordId(params.run.id, TABLES.PLAN_RUN),
|
|
1859
|
-
eventType: params.eventType,
|
|
1860
|
-
message: params.message,
|
|
1861
|
-
emittedBy: params.emittedBy,
|
|
1862
|
-
...(params.nodeId ? { nodeId: params.nodeId } : {}),
|
|
1863
|
-
...(params.attemptId ? { attemptId: ensureRecordId(params.attemptId, TABLES.PLAN_NODE_ATTEMPT) } : {}),
|
|
1864
|
-
...(params.approvalId ? { approvalId: ensureRecordId(params.approvalId, TABLES.PLAN_APPROVAL) } : {}),
|
|
1865
|
-
...(params.fromStatus ? { fromStatus: params.fromStatus } : {}),
|
|
1866
|
-
...(params.toStatus ? { toStatus: params.toStatus } : {}),
|
|
1867
|
-
...(params.detail ? { detail: params.detail } : {}),
|
|
1868
|
-
})
|
|
1869
|
-
.output('after')
|
|
1870
|
-
|
|
1871
|
-
const event = PlanEventSchema.parse(created)
|
|
1872
|
-
params.capturedEvents?.push(event)
|
|
1873
|
-
return event
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
private async replaceRun(tx: DatabaseTransaction, run: PlanRunRecord, patch: PlanRunUpdate): Promise<PlanRunRecord> {
|
|
1877
|
-
return PlanRunSchema.parse(
|
|
1878
|
-
await tx.update(ensureRecordId(run.id, TABLES.PLAN_RUN)).content(toRunData(run, patch)).output('after'),
|
|
1879
|
-
)
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
private async saveCheckpoint(params: {
|
|
1883
|
-
tx: DatabaseTransaction
|
|
1884
|
-
run: PlanRunRecord
|
|
1885
|
-
spec: PlanSpecRecord
|
|
1886
|
-
nodeRuns: PlanNodeRunRecord[]
|
|
1887
|
-
artifacts: Array<{ id: RecordIdInput; nodeId: string }>
|
|
1888
|
-
sequence: number
|
|
1889
|
-
reason: string
|
|
1890
|
-
includeWorkspace?: boolean
|
|
1891
|
-
capturedEvents?: PlanEventRecord[]
|
|
1892
|
-
}) {
|
|
1893
|
-
const checkpoint = await planCheckpointService.createCheckpoint({
|
|
1894
|
-
tx: params.tx,
|
|
1895
|
-
runId: params.run.id,
|
|
1896
|
-
sequence: params.sequence,
|
|
1897
|
-
runStatus: params.run.status,
|
|
1898
|
-
readyNodeIds: params.run.readyNodeIds,
|
|
1899
|
-
activeNodeIds: params.run.currentNodeId ? [params.run.currentNodeId] : [],
|
|
1900
|
-
artifactIds: params.artifacts.map((artifact) => artifact.id),
|
|
1901
|
-
lastCompletedNodeIds: params.nodeRuns
|
|
1902
|
-
.filter((nodeRun) => nodeRun.status === 'completed' || nodeRun.status === 'partial')
|
|
1903
|
-
.map((nodeRun) => nodeRun.nodeId),
|
|
1904
|
-
snapshot: {
|
|
1905
|
-
reason: params.reason,
|
|
1906
|
-
runStatus: params.run.status,
|
|
1907
|
-
currentNodeId: params.run.currentNodeId,
|
|
1908
|
-
waitingNodeId: params.run.waitingNodeId,
|
|
1909
|
-
readyNodeIds: params.run.readyNodeIds,
|
|
1910
|
-
nodeStatuses: Object.fromEntries(params.nodeRuns.map((nodeRun) => [nodeRun.nodeId, nodeRun.status])),
|
|
1911
|
-
},
|
|
1912
|
-
includeWorkspace: params.includeWorkspace,
|
|
1913
|
-
})
|
|
1914
|
-
|
|
1915
|
-
await this.emitEvent({
|
|
1916
|
-
tx: params.tx,
|
|
1917
|
-
run: params.run,
|
|
1918
|
-
spec: params.spec,
|
|
1919
|
-
eventType: 'checkpoint-saved',
|
|
1920
|
-
message: `Saved checkpoint ${checkpoint.sequence}.`,
|
|
1921
|
-
detail: { checkpointId: recordIdToString(checkpoint.id, TABLES.PLAN_CHECKPOINT), reason: params.reason },
|
|
1922
|
-
emittedBy: 'system',
|
|
1923
|
-
capturedEvents: params.capturedEvents,
|
|
1924
|
-
})
|
|
1925
|
-
|
|
1926
|
-
return checkpoint
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
private async attachCheckpoint(
|
|
1930
|
-
tx: DatabaseTransaction,
|
|
1931
|
-
run: PlanRunRecord,
|
|
1932
|
-
checkpoint: RecordIdInput | { id: RecordIdInput },
|
|
1933
|
-
) {
|
|
1934
|
-
const checkpointId =
|
|
1935
|
-
checkpoint &&
|
|
1936
|
-
typeof checkpoint === 'object' &&
|
|
1937
|
-
!(checkpoint instanceof RecordId) &&
|
|
1938
|
-
!(checkpoint instanceof StringRecordId) &&
|
|
1939
|
-
'id' in checkpoint
|
|
1940
|
-
? (checkpoint as { id: RecordIdInput }).id
|
|
1941
|
-
: checkpoint
|
|
1942
|
-
|
|
1943
|
-
await tx
|
|
1944
|
-
.update(ensureRecordId(run.id, TABLES.PLAN_RUN))
|
|
1945
|
-
.content(toRunData(run, { lastCheckpointId: checkpointId }))
|
|
1946
|
-
.output('after')
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
export const planExecutorService = new PlanExecutorService()
|