@lota-sdk/core 0.4.7 → 0.4.9
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 +94 -22
- package/src/ai-gateway/ai-gateway.ts +738 -223
- package/src/config/agent-defaults.ts +176 -75
- package/src/config/agent-types.ts +54 -4
- package/src/config/constants.ts +8 -2
- package/src/config/logger.ts +286 -19
- package/src/config/model-constants.ts +1 -0
- package/src/config/thread-defaults.ts +33 -21
- package/src/create-runtime.ts +725 -383
- package/src/db/base.service.ts +52 -28
- package/src/db/cursor-pagination.ts +71 -30
- package/src/db/memory-store.helpers.ts +4 -7
- package/src/db/memory-store.ts +856 -598
- package/src/db/memory.ts +398 -275
- package/src/db/record-id.ts +32 -10
- package/src/db/schema-fingerprint.ts +30 -12
- package/src/db/service-normalization.ts +255 -0
- package/src/db/service.ts +726 -761
- package/src/db/startup.ts +140 -66
- package/src/db/transaction-conflict.ts +15 -0
- package/src/effect/awaitable-effect.ts +87 -0
- package/src/effect/errors.ts +121 -0
- package/src/effect/helpers.ts +98 -0
- package/src/effect/index.ts +22 -0
- package/src/effect/layers.ts +228 -0
- package/src/effect/runtime-ref.ts +25 -0
- package/src/effect/runtime.ts +31 -0
- package/src/effect/services.ts +57 -0
- package/src/effect/zod.ts +43 -0
- package/src/embeddings/provider.ts +122 -71
- package/src/index.ts +46 -1
- package/src/openrouter/direct-provider.ts +29 -0
- package/src/queues/autonomous-job.queue.ts +130 -74
- package/src/queues/context-compaction.queue.ts +60 -15
- package/src/queues/delayed-node-promotion.queue.ts +52 -15
- 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 +13 -4
- package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
- package/src/queues/plan-scheduler.queue.ts +107 -31
- package/src/queues/post-chat-memory.queue.ts +66 -24
- package/src/queues/queue-factory.ts +142 -52
- package/src/queues/standalone-worker.ts +39 -0
- package/src/queues/title-generation.queue.ts +54 -9
- package/src/redis/connection.ts +84 -32
- 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 +10 -0
- package/src/redis/stream-context.ts +84 -46
- package/src/runtime/agent-identity-overrides.ts +2 -2
- package/src/runtime/agent-runtime-policy.ts +4 -1
- package/src/runtime/agent-stream-helpers.ts +20 -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} +114 -91
- package/src/runtime/execution-plan-visibility.ts +2 -2
- package/src/runtime/execution-plan.ts +42 -15
- package/src/runtime/graph-designer.ts +11 -7
- package/src/runtime/helper-model.ts +135 -48
- package/src/runtime/index.ts +7 -7
- package/src/runtime/indexed-repositories-policy.ts +3 -3
- package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -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} +1 -1
- package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
- package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
- package/src/runtime/plugin-resolution.ts +144 -24
- package/src/runtime/plugin-types.ts +9 -1
- package/src/runtime/post-turn-side-effects.ts +197 -130
- package/src/runtime/retrieval-adapters.ts +38 -4
- package/src/runtime/runtime-config.ts +165 -61
- package/src/runtime/runtime-extensions.ts +21 -34
- package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
- package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
- package/src/runtime/social-chat/social-chat.ts +594 -0
- package/src/runtime/specialist-runner.ts +36 -10
- package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -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 +172 -94
- package/src/runtime/turn-lifecycle.ts +93 -27
- package/src/services/agent-activity.service.ts +287 -203
- package/src/services/agent-executor.service.ts +329 -217
- package/src/services/artifact.service.ts +225 -148
- package/src/services/attachment.service.ts +137 -115
- package/src/services/autonomous-job.service.ts +888 -491
- package/src/services/chat-run-registry.service.ts +11 -1
- package/src/services/context-compaction.service.ts +136 -86
- package/src/services/document-chunk.service.ts +162 -90
- 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 +256 -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 +80 -170
- package/src/services/graph-full-routing.ts +182 -0
- package/src/services/index.ts +18 -20
- package/src/services/institutional-memory.service.ts +220 -123
- package/src/services/learned-skill.service.ts +364 -259
- package/src/services/memory/memory-conversation.ts +95 -0
- package/src/services/memory/memory-org-memory.ts +39 -0
- package/src/services/memory/memory-preseeded.ts +80 -0
- package/src/services/memory/memory-rerank.ts +297 -0
- package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
- package/src/services/memory/memory.service.ts +692 -0
- package/src/services/memory/rerank.service.ts +209 -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 +17 -16
- package/src/services/organization-member.service.ts +120 -66
- package/src/services/organization.service.ts +144 -51
- package/src/services/ownership-dispatcher.service.ts +415 -264
- 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/plan-approval.service.ts +102 -0
- 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 +175 -0
- package/src/services/plan/plan-coordination.service.ts +181 -0
- package/src/services/plan/plan-cycle.service.ts +398 -0
- package/src/services/plan/plan-deadline.service.ts +547 -0
- package/src/services/plan/plan-event-delivery.service.ts +261 -0
- package/src/services/plan/plan-executor-context.ts +35 -0
- package/src/services/plan/plan-executor-graph.ts +475 -0
- package/src/services/plan/plan-executor-helpers.ts +322 -0
- package/src/services/plan/plan-executor-persistence.ts +209 -0
- package/src/services/plan/plan-executor.service.ts +1654 -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 +644 -0
- package/src/services/plan/plan-scheduler.service.ts +385 -0
- package/src/services/plan/plan-template.service.ts +224 -0
- package/src/services/plan/plan-transaction-events.ts +33 -0
- package/src/services/plan/plan-validator.service.ts +907 -0
- package/src/services/plan/plan-workspace.service.ts +125 -0
- package/src/services/plugin-executor.service.ts +97 -68
- package/src/services/quality-metrics.service.ts +112 -94
- package/src/services/queue-job.service.ts +296 -230
- package/src/services/recent-activity-title.service.ts +65 -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 +176 -125
- package/src/services/system-executor.service.ts +91 -61
- package/src/services/thread/thread-active-run.ts +203 -0
- package/src/services/thread/thread-bootstrap.ts +369 -0
- package/src/services/thread/thread-listing.ts +198 -0
- package/src/services/thread/thread-memory-block.ts +117 -0
- package/src/services/thread/thread-message.service.ts +363 -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 +1146 -0
- package/src/services/thread/thread-turn-streaming.ts +402 -0
- package/src/services/thread/thread-turn-tracing.ts +35 -0
- package/src/services/thread/thread-turn.ts +343 -0
- package/src/services/thread/thread.service.ts +335 -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 +331 -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 +2 -2
- package/src/system-agents/delegated-agent-factory.ts +159 -90
- package/src/system-agents/memory-reranker.agent.ts +2 -2
- package/src/system-agents/memory.agent.ts +2 -2
- package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
- package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
- package/src/system-agents/skill-extractor.agent.ts +2 -2
- package/src/system-agents/skill-manager.agent.ts +2 -2
- package/src/system-agents/thread-router.agent.ts +157 -113
- package/src/system-agents/title-generator.agent.ts +2 -2
- package/src/tools/execution-plan.tool.ts +220 -161
- package/src/tools/fetch-webpage.tool.ts +21 -17
- package/src/tools/firecrawl-client.ts +16 -6
- package/src/tools/index.ts +1 -0
- package/src/tools/memory-block.tool.ts +14 -6
- package/src/tools/plan-approval.tool.ts +49 -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 +26 -22
- package/src/tools/search.tool.ts +41 -29
- package/src/tools/team-think.tool.ts +124 -83
- package/src/tools/user-questions.tool.ts +4 -3
- package/src/tools/web-tool-shared.ts +6 -0
- package/src/utils/async.ts +17 -23
- package/src/utils/crypto.ts +21 -0
- package/src/utils/date-time.ts +40 -1
- package/src/utils/errors.ts +95 -16
- 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 +186 -51
- 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 +175 -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 +56 -31
- package/src/config/debug-logger.ts +0 -47
- package/src/redis/connection-accessor.ts +0 -26
- package/src/runtime/context-compaction-runtime.ts +0 -87
- 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 -844
- package/src/services/plan-agent-heartbeat.service.ts +0 -136
- package/src/services/plan-agent-query.service.ts +0 -267
- package/src/services/plan-approval.service.ts +0 -83
- 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/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,818 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PlanCompletionCheck,
|
|
3
|
-
PlanDataSchema,
|
|
4
|
-
PlanDraft,
|
|
5
|
-
PlanFailureClass,
|
|
6
|
-
PlanNodeType,
|
|
7
|
-
PlanNodeResultSubmission,
|
|
8
|
-
PlanValidationIssueSeverity,
|
|
9
|
-
} from '@lota-sdk/shared'
|
|
10
|
-
import { HUMAN_NODE_TYPES, STRUCTURAL_NODE_TYPES } from '@lota-sdk/shared'
|
|
11
|
-
|
|
12
|
-
import { isRecord } from '../utils/string'
|
|
13
|
-
import { planCoordinationService } from './plan-coordination.service'
|
|
14
|
-
import { readPathValue } from './plan-helpers'
|
|
15
|
-
import type { PlanNodeValidationSpec } from './plan-node-spec'
|
|
16
|
-
|
|
17
|
-
const STRUCTURAL_NODE_TYPE_SET = new Set<PlanNodeType>(STRUCTURAL_NODE_TYPES)
|
|
18
|
-
const HUMAN_NODE_TYPE_SET = new Set<PlanNodeType>(HUMAN_NODE_TYPES)
|
|
19
|
-
|
|
20
|
-
export interface PlanValidationIssueInput {
|
|
21
|
-
severity: PlanValidationIssueSeverity
|
|
22
|
-
code: string
|
|
23
|
-
message: string
|
|
24
|
-
detail?: Record<string, unknown>
|
|
25
|
-
nodeId?: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface DraftValidationResult {
|
|
29
|
-
blocking: PlanValidationIssueInput[]
|
|
30
|
-
warnings: PlanValidationIssueInput[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface NodeResultValidationResult {
|
|
34
|
-
blocking: PlanValidationIssueInput[]
|
|
35
|
-
warnings: PlanValidationIssueInput[]
|
|
36
|
-
failureClass: PlanFailureClass | null
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function createIssue(params: {
|
|
40
|
-
code: string
|
|
41
|
-
message: string
|
|
42
|
-
severity?: PlanValidationIssueSeverity
|
|
43
|
-
detail?: Record<string, unknown>
|
|
44
|
-
nodeId?: string
|
|
45
|
-
}): PlanValidationIssueInput {
|
|
46
|
-
return {
|
|
47
|
-
severity: params.severity ?? 'blocking',
|
|
48
|
-
code: params.code,
|
|
49
|
-
message: params.message,
|
|
50
|
-
...(params.detail ? { detail: params.detail } : {}),
|
|
51
|
-
...(params.nodeId ? { nodeId: params.nodeId } : {}),
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function hasAllFields(value: unknown, fields: string[]): boolean {
|
|
56
|
-
if (!isRecord(value)) return false
|
|
57
|
-
return fields.every((field) => readPathValue(value, field) !== undefined)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function validateSchemaValue(params: { schema: PlanDataSchema; value: unknown; path: string }): string[] {
|
|
61
|
-
const issues: string[] = []
|
|
62
|
-
const { schema, value, path } = params
|
|
63
|
-
|
|
64
|
-
if (value === null || value === undefined) {
|
|
65
|
-
if (schema.required || (!schema.nullable && value === null)) {
|
|
66
|
-
issues.push(`${path} is required`)
|
|
67
|
-
}
|
|
68
|
-
return issues
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
|
|
72
|
-
issues.push(`${path} must be one of the allowed enum values`)
|
|
73
|
-
return issues
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (schema.type === 'string') {
|
|
77
|
-
if (typeof value !== 'string') issues.push(`${path} must be a string`)
|
|
78
|
-
return issues
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (schema.type === 'number') {
|
|
82
|
-
if (typeof value !== 'number' || Number.isNaN(value)) issues.push(`${path} must be a number`)
|
|
83
|
-
return issues
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (schema.type === 'boolean') {
|
|
87
|
-
if (typeof value !== 'boolean') issues.push(`${path} must be a boolean`)
|
|
88
|
-
return issues
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (schema.type === 'array') {
|
|
92
|
-
if (!Array.isArray(value)) {
|
|
93
|
-
issues.push(`${path} must be an array`)
|
|
94
|
-
return issues
|
|
95
|
-
}
|
|
96
|
-
if (schema.minItems !== undefined && value.length < schema.minItems) {
|
|
97
|
-
issues.push(`${path} must contain at least ${schema.minItems} item(s)`)
|
|
98
|
-
}
|
|
99
|
-
if (schema.maxItems !== undefined && value.length > schema.maxItems) {
|
|
100
|
-
issues.push(`${path} must contain at most ${schema.maxItems} item(s)`)
|
|
101
|
-
}
|
|
102
|
-
const itemSchema = schema.items
|
|
103
|
-
if (itemSchema) {
|
|
104
|
-
value.forEach((entry, index) => {
|
|
105
|
-
issues.push(...validateSchemaValue({ schema: itemSchema, value: entry, path: `${path}[${index}]` }))
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
return issues
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!isRecord(value)) {
|
|
112
|
-
issues.push(`${path} must be an object`)
|
|
113
|
-
return issues
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
|
|
117
|
-
issues.push(...validateSchemaValue({ schema: childSchema, value: value[key], path: `${path}.${key}` }))
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return issues
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function buildReachableNodeIds(entryNodeIds: string[], adjacency: Map<string, string[]>): Set<string> {
|
|
124
|
-
const visited = new Set<string>()
|
|
125
|
-
const queue = [...entryNodeIds]
|
|
126
|
-
|
|
127
|
-
while (queue.length > 0) {
|
|
128
|
-
const nodeId = queue.shift()
|
|
129
|
-
if (!nodeId || visited.has(nodeId)) continue
|
|
130
|
-
visited.add(nodeId)
|
|
131
|
-
for (const next of adjacency.get(nodeId) ?? []) {
|
|
132
|
-
if (!visited.has(next)) {
|
|
133
|
-
queue.push(next)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return visited
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function graphHasCycle(nodeIds: string[], adjacency: Map<string, string[]>): boolean {
|
|
142
|
-
const visiting = new Set<string>()
|
|
143
|
-
const visited = new Set<string>()
|
|
144
|
-
|
|
145
|
-
const visit = (nodeId: string): boolean => {
|
|
146
|
-
if (visited.has(nodeId)) return false
|
|
147
|
-
if (visiting.has(nodeId)) return true
|
|
148
|
-
|
|
149
|
-
visiting.add(nodeId)
|
|
150
|
-
for (const next of adjacency.get(nodeId) ?? []) {
|
|
151
|
-
if (visit(next)) return true
|
|
152
|
-
}
|
|
153
|
-
visiting.delete(nodeId)
|
|
154
|
-
visited.add(nodeId)
|
|
155
|
-
return false
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return nodeIds.some((nodeId) => visit(nodeId))
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveSchemaRef(
|
|
162
|
-
draft: Pick<PlanDraft, 'schemas'>,
|
|
163
|
-
schemaRef: string | null | undefined,
|
|
164
|
-
): PlanDataSchema | undefined {
|
|
165
|
-
if (!schemaRef) return undefined
|
|
166
|
-
return draft.schemas[schemaRef]
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
class PlanValidatorService {
|
|
170
|
-
validateDraft(draft: PlanDraft): DraftValidationResult {
|
|
171
|
-
const blocking: PlanValidationIssueInput[] = []
|
|
172
|
-
const warnings: PlanValidationIssueInput[] = []
|
|
173
|
-
const nodeIds = new Set<string>()
|
|
174
|
-
const edgeIds = new Set<string>()
|
|
175
|
-
const nodesById = new Map(draft.nodes.map((node) => [node.id, node]))
|
|
176
|
-
const adjacency = new Map<string, string[]>(draft.nodes.map((node) => [node.id, []]))
|
|
177
|
-
const inbound = new Map<string, string[]>(draft.nodes.map((node) => [node.id, []]))
|
|
178
|
-
|
|
179
|
-
for (const node of draft.nodes) {
|
|
180
|
-
if (nodeIds.has(node.id)) {
|
|
181
|
-
blocking.push(
|
|
182
|
-
createIssue({
|
|
183
|
-
code: 'duplicate_node_id',
|
|
184
|
-
message: `Plan contains duplicate node id "${node.id}".`,
|
|
185
|
-
nodeId: node.id,
|
|
186
|
-
}),
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
nodeIds.add(node.id)
|
|
190
|
-
|
|
191
|
-
const isStructuralNode = STRUCTURAL_NODE_TYPE_SET.has(node.type)
|
|
192
|
-
if (!isStructuralNode && node.deliverables.length === 0) {
|
|
193
|
-
blocking.push(
|
|
194
|
-
createIssue({
|
|
195
|
-
code: 'node_missing_deliverables',
|
|
196
|
-
message: `Node "${node.label}" must define at least one deliverable.`,
|
|
197
|
-
nodeId: node.id,
|
|
198
|
-
}),
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
if (node.successCriteria.length === 0) {
|
|
202
|
-
blocking.push(
|
|
203
|
-
createIssue({
|
|
204
|
-
code: 'node_missing_success_criteria',
|
|
205
|
-
message: `Node "${node.label}" must define at least one success criterion.`,
|
|
206
|
-
nodeId: node.id,
|
|
207
|
-
}),
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
if (!isStructuralNode && node.completionChecks.length === 0) {
|
|
211
|
-
blocking.push(
|
|
212
|
-
createIssue({
|
|
213
|
-
code: 'node_missing_completion_checks',
|
|
214
|
-
message: `Node "${node.label}" must define at least one completion check.`,
|
|
215
|
-
nodeId: node.id,
|
|
216
|
-
}),
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
if (HUMAN_NODE_TYPE_SET.has(node.type) && node.owner.executorType !== 'user') {
|
|
220
|
-
blocking.push(
|
|
221
|
-
createIssue({
|
|
222
|
-
code: 'human_node_owner_mismatch',
|
|
223
|
-
message: `Human node "${node.label}" must be owned by a user executor.`,
|
|
224
|
-
nodeId: node.id,
|
|
225
|
-
}),
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
if (
|
|
229
|
-
!HUMAN_NODE_TYPE_SET.has(node.type) &&
|
|
230
|
-
node.type !== 'join' &&
|
|
231
|
-
node.type !== 'switch' &&
|
|
232
|
-
node.owner.ref.trim().length === 0
|
|
233
|
-
) {
|
|
234
|
-
blocking.push(
|
|
235
|
-
createIssue({
|
|
236
|
-
code: 'node_owner_missing',
|
|
237
|
-
message: `Node "${node.label}" must declare an executor reference.`,
|
|
238
|
-
nodeId: node.id,
|
|
239
|
-
}),
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
if (node.owner.executorType === 'plugin' && node.owner.operation.trim().length === 0) {
|
|
243
|
-
blocking.push(
|
|
244
|
-
createIssue({
|
|
245
|
-
code: 'plugin_owner_operation_missing',
|
|
246
|
-
message: `Plugin-owned node "${node.label}" must declare an operation.`,
|
|
247
|
-
nodeId: node.id,
|
|
248
|
-
}),
|
|
249
|
-
)
|
|
250
|
-
}
|
|
251
|
-
if (node.owner.executorType === 'system' && node.owner.operation.trim().length === 0) {
|
|
252
|
-
blocking.push(
|
|
253
|
-
createIssue({
|
|
254
|
-
code: 'system_owner_operation_missing',
|
|
255
|
-
message: `System-owned node "${node.label}" must declare an operation.`,
|
|
256
|
-
nodeId: node.id,
|
|
257
|
-
}),
|
|
258
|
-
)
|
|
259
|
-
}
|
|
260
|
-
if (node.inputSchemaRef && !resolveSchemaRef(draft, node.inputSchemaRef)) {
|
|
261
|
-
blocking.push(
|
|
262
|
-
createIssue({
|
|
263
|
-
code: 'missing_input_schema_ref',
|
|
264
|
-
message: `Node "${node.label}" references unknown input schema "${node.inputSchemaRef}".`,
|
|
265
|
-
nodeId: node.id,
|
|
266
|
-
}),
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
if (node.outputSchemaRef && !resolveSchemaRef(draft, node.outputSchemaRef)) {
|
|
270
|
-
blocking.push(
|
|
271
|
-
createIssue({
|
|
272
|
-
code: 'missing_output_schema_ref',
|
|
273
|
-
message: `Node "${node.label}" references unknown output schema "${node.outputSchemaRef}".`,
|
|
274
|
-
nodeId: node.id,
|
|
275
|
-
}),
|
|
276
|
-
)
|
|
277
|
-
}
|
|
278
|
-
for (const deliverable of node.deliverables) {
|
|
279
|
-
if (deliverable.schemaRef && !resolveSchemaRef(draft, deliverable.schemaRef)) {
|
|
280
|
-
blocking.push(
|
|
281
|
-
createIssue({
|
|
282
|
-
code: 'missing_artifact_schema_ref',
|
|
283
|
-
message: `Node "${node.label}" references unknown artifact schema "${deliverable.schemaRef}".`,
|
|
284
|
-
nodeId: node.id,
|
|
285
|
-
detail: { artifactName: deliverable.name },
|
|
286
|
-
}),
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
for (const check of node.completionChecks) {
|
|
291
|
-
const checkSchemaRef = typeof check.config.schemaRef === 'string' ? check.config.schemaRef : undefined
|
|
292
|
-
if (checkSchemaRef && !resolveSchemaRef(draft, checkSchemaRef)) {
|
|
293
|
-
blocking.push(
|
|
294
|
-
createIssue({
|
|
295
|
-
code: 'missing_completion_check_schema_ref',
|
|
296
|
-
message: `Node "${node.label}" references unknown completion-check schema "${checkSchemaRef}".`,
|
|
297
|
-
nodeId: node.id,
|
|
298
|
-
detail: { checkDescription: check.description },
|
|
299
|
-
}),
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
if (node.toolPolicy.allow.some((toolName) => node.toolPolicy.deny.includes(toolName))) {
|
|
304
|
-
warnings.push(
|
|
305
|
-
createIssue({
|
|
306
|
-
code: 'tool_policy_overlap',
|
|
307
|
-
severity: 'warning',
|
|
308
|
-
message: `Node "${node.label}" lists the same tool in allow and deny policy.`,
|
|
309
|
-
nodeId: node.id,
|
|
310
|
-
}),
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Validate deliberation-fork nodes
|
|
315
|
-
if (node.type === 'deliberation-fork') {
|
|
316
|
-
if (!node.deliberationConfig) {
|
|
317
|
-
blocking.push(
|
|
318
|
-
createIssue({
|
|
319
|
-
code: 'deliberation_fork_missing_config',
|
|
320
|
-
message: `Deliberation-fork node "${node.label}" must define deliberationConfig.`,
|
|
321
|
-
nodeId: node.id,
|
|
322
|
-
}),
|
|
323
|
-
)
|
|
324
|
-
} else {
|
|
325
|
-
if (node.deliberationConfig.branches.length < 2) {
|
|
326
|
-
blocking.push(
|
|
327
|
-
createIssue({
|
|
328
|
-
code: 'deliberation_fork_insufficient_branches',
|
|
329
|
-
message: `Deliberation-fork node "${node.label}" must define at least 2 branches.`,
|
|
330
|
-
nodeId: node.id,
|
|
331
|
-
}),
|
|
332
|
-
)
|
|
333
|
-
}
|
|
334
|
-
for (const branch of node.deliberationConfig.branches) {
|
|
335
|
-
if (!nodesById.has(branch.entryNodeId)) {
|
|
336
|
-
blocking.push(
|
|
337
|
-
createIssue({
|
|
338
|
-
code: 'deliberation_fork_missing_branch_entry',
|
|
339
|
-
message: `Deliberation-fork node "${node.label}" references missing branch entry node "${branch.entryNodeId}".`,
|
|
340
|
-
nodeId: node.id,
|
|
341
|
-
detail: { branchId: branch.branchId, entryNodeId: branch.entryNodeId },
|
|
342
|
-
}),
|
|
343
|
-
)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
const gateNode = nodesById.get(node.deliberationConfig.resolutionGateNodeId)
|
|
347
|
-
if (!gateNode) {
|
|
348
|
-
blocking.push(
|
|
349
|
-
createIssue({
|
|
350
|
-
code: 'deliberation_fork_missing_gate',
|
|
351
|
-
message: `Deliberation-fork node "${node.label}" references missing resolution gate node "${node.deliberationConfig.resolutionGateNodeId}".`,
|
|
352
|
-
nodeId: node.id,
|
|
353
|
-
detail: { resolutionGateNodeId: node.deliberationConfig.resolutionGateNodeId },
|
|
354
|
-
}),
|
|
355
|
-
)
|
|
356
|
-
} else if (gateNode.type !== 'human-decision') {
|
|
357
|
-
blocking.push(
|
|
358
|
-
createIssue({
|
|
359
|
-
code: 'deliberation_fork_gate_type_mismatch',
|
|
360
|
-
message: `Resolution gate node "${gateNode.label}" must be of type "human-decision".`,
|
|
361
|
-
nodeId: node.id,
|
|
362
|
-
detail: { gateNodeId: gateNode.id, gateNodeType: gateNode.type },
|
|
363
|
-
}),
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const entryNodeIds = draft.entryNodeIds ?? []
|
|
371
|
-
for (const entryNodeId of entryNodeIds) {
|
|
372
|
-
if (!nodesById.has(entryNodeId)) {
|
|
373
|
-
blocking.push(
|
|
374
|
-
createIssue({
|
|
375
|
-
code: 'missing_entry_node',
|
|
376
|
-
message: `Entry node "${entryNodeId}" does not exist in the plan.`,
|
|
377
|
-
detail: { entryNodeId },
|
|
378
|
-
}),
|
|
379
|
-
)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
for (const edge of draft.edges) {
|
|
384
|
-
if (edgeIds.has(edge.id)) {
|
|
385
|
-
blocking.push(
|
|
386
|
-
createIssue({
|
|
387
|
-
code: 'duplicate_edge_id',
|
|
388
|
-
message: `Plan contains duplicate edge id "${edge.id}".`,
|
|
389
|
-
detail: { edgeId: edge.id },
|
|
390
|
-
}),
|
|
391
|
-
)
|
|
392
|
-
}
|
|
393
|
-
edgeIds.add(edge.id)
|
|
394
|
-
|
|
395
|
-
if (!nodesById.has(edge.source)) {
|
|
396
|
-
blocking.push(
|
|
397
|
-
createIssue({
|
|
398
|
-
code: 'missing_edge_source',
|
|
399
|
-
message: `Edge "${edge.id}" references missing source node "${edge.source}".`,
|
|
400
|
-
detail: { edgeId: edge.id, source: edge.source },
|
|
401
|
-
}),
|
|
402
|
-
)
|
|
403
|
-
continue
|
|
404
|
-
}
|
|
405
|
-
if (!nodesById.has(edge.target)) {
|
|
406
|
-
blocking.push(
|
|
407
|
-
createIssue({
|
|
408
|
-
code: 'missing_edge_target',
|
|
409
|
-
message: `Edge "${edge.id}" references missing target node "${edge.target}".`,
|
|
410
|
-
detail: { edgeId: edge.id, target: edge.target },
|
|
411
|
-
}),
|
|
412
|
-
)
|
|
413
|
-
continue
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
adjacency.get(edge.source)?.push(edge.target)
|
|
417
|
-
inbound.get(edge.target)?.push(edge.source)
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
for (const node of draft.nodes) {
|
|
421
|
-
const inboundEdges = inbound.get(node.id) ?? []
|
|
422
|
-
const outboundEdges = adjacency.get(node.id) ?? []
|
|
423
|
-
|
|
424
|
-
if (node.type === 'join' && inboundEdges.length < 2) {
|
|
425
|
-
blocking.push(
|
|
426
|
-
createIssue({
|
|
427
|
-
code: 'join_requires_multiple_inbound_edges',
|
|
428
|
-
message: `Join node "${node.label}" must have at least two inbound edges.`,
|
|
429
|
-
nodeId: node.id,
|
|
430
|
-
}),
|
|
431
|
-
)
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (node.type === 'switch') {
|
|
435
|
-
const conditionalEdges = draft.edges.filter((edge) => edge.source === node.id && edge.when)
|
|
436
|
-
if (conditionalEdges.length === 0) {
|
|
437
|
-
blocking.push(
|
|
438
|
-
createIssue({
|
|
439
|
-
code: 'switch_requires_conditional_edges',
|
|
440
|
-
message: `Switch node "${node.label}" must define conditional outbound edges.`,
|
|
441
|
-
nodeId: node.id,
|
|
442
|
-
}),
|
|
443
|
-
)
|
|
444
|
-
}
|
|
445
|
-
if (outboundEdges.length < 2) {
|
|
446
|
-
warnings.push(
|
|
447
|
-
createIssue({
|
|
448
|
-
code: 'switch_has_single_path',
|
|
449
|
-
severity: 'warning',
|
|
450
|
-
message: `Switch node "${node.label}" only routes to one target.`,
|
|
451
|
-
nodeId: node.id,
|
|
452
|
-
}),
|
|
453
|
-
)
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
if (!entryNodeIds.includes(node.id) && inboundEdges.length === 0) {
|
|
458
|
-
blocking.push(
|
|
459
|
-
createIssue({
|
|
460
|
-
code: 'orphan_node',
|
|
461
|
-
message: `Node "${node.label}" is not connected to the graph.`,
|
|
462
|
-
nodeId: node.id,
|
|
463
|
-
}),
|
|
464
|
-
)
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (node.inputSchemaRef && inboundEdges.length > 0) {
|
|
468
|
-
const hasMappedInput = draft.edges
|
|
469
|
-
.filter((edge) => edge.target === node.id)
|
|
470
|
-
.some((edge) => Object.keys(edge.map).length > 0)
|
|
471
|
-
if (!hasMappedInput) {
|
|
472
|
-
blocking.push(
|
|
473
|
-
createIssue({
|
|
474
|
-
code: 'unresolved_node_input',
|
|
475
|
-
message: `Node "${node.label}" declares inputSchemaRef but no inbound edge maps data into it.`,
|
|
476
|
-
nodeId: node.id,
|
|
477
|
-
}),
|
|
478
|
-
)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const reachableNodeIds = buildReachableNodeIds(entryNodeIds, adjacency)
|
|
484
|
-
for (const node of draft.nodes) {
|
|
485
|
-
if (!reachableNodeIds.has(node.id)) {
|
|
486
|
-
blocking.push(
|
|
487
|
-
createIssue({
|
|
488
|
-
code: 'unreachable_node',
|
|
489
|
-
message: `Node "${node.label}" is unreachable from the configured entry nodes.`,
|
|
490
|
-
nodeId: node.id,
|
|
491
|
-
}),
|
|
492
|
-
)
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
graphHasCycle(
|
|
498
|
-
draft.nodes.map((node) => node.id),
|
|
499
|
-
adjacency,
|
|
500
|
-
)
|
|
501
|
-
) {
|
|
502
|
-
blocking.push(
|
|
503
|
-
createIssue({
|
|
504
|
-
code: 'cycle_detected',
|
|
505
|
-
message: 'Plan graph contains a cycle. Loop semantics are not supported in this cutover.',
|
|
506
|
-
}),
|
|
507
|
-
)
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Validate cross-plan dependency cycles
|
|
511
|
-
if (draft.dependencies && draft.dependencies.length > 0) {
|
|
512
|
-
const cycleIssues = planCoordinationService.validateNoCycles([
|
|
513
|
-
{ id: draft.title, title: draft.title, dependencies: draft.dependencies },
|
|
514
|
-
])
|
|
515
|
-
blocking.push(...cycleIssues)
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
return { blocking, warnings }
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
validateNodeResult(params: {
|
|
522
|
-
draft: Pick<PlanDraft, 'schemas'>
|
|
523
|
-
node: PlanNodeValidationSpec
|
|
524
|
-
result: PlanNodeResultSubmission
|
|
525
|
-
}): NodeResultValidationResult {
|
|
526
|
-
const blocking: PlanValidationIssueInput[] = []
|
|
527
|
-
const warnings: PlanValidationIssueInput[] = []
|
|
528
|
-
const artifactsByName = new Map(params.result.artifacts.map((artifact) => [artifact.name, artifact]))
|
|
529
|
-
|
|
530
|
-
if (params.node.outputSchemaRef) {
|
|
531
|
-
const outputSchema = resolveSchemaRef(params.draft, params.node.outputSchemaRef)
|
|
532
|
-
if (!params.result.structuredOutput) {
|
|
533
|
-
blocking.push(
|
|
534
|
-
createIssue({
|
|
535
|
-
code: 'structured_output_missing',
|
|
536
|
-
message: `Node "${params.node.label}" requires structured output.`,
|
|
537
|
-
nodeId: params.node.id,
|
|
538
|
-
}),
|
|
539
|
-
)
|
|
540
|
-
} else if (outputSchema) {
|
|
541
|
-
const schemaIssues = validateSchemaValue({
|
|
542
|
-
schema: outputSchema,
|
|
543
|
-
value: params.result.structuredOutput,
|
|
544
|
-
path: 'structuredOutput',
|
|
545
|
-
})
|
|
546
|
-
if (schemaIssues.length > 0) {
|
|
547
|
-
blocking.push(
|
|
548
|
-
createIssue({
|
|
549
|
-
code: 'schema_validation_failed',
|
|
550
|
-
message: `Structured output for node "${params.node.label}" failed schema validation.`,
|
|
551
|
-
nodeId: params.node.id,
|
|
552
|
-
detail: { issues: schemaIssues },
|
|
553
|
-
}),
|
|
554
|
-
)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
for (const deliverable of params.node.deliverables) {
|
|
560
|
-
const artifact = artifactsByName.get(deliverable.name)
|
|
561
|
-
if (!artifact) {
|
|
562
|
-
if (deliverable.required) {
|
|
563
|
-
blocking.push(
|
|
564
|
-
createIssue({
|
|
565
|
-
code: 'required_artifact_missing',
|
|
566
|
-
message: `Node "${params.node.label}" is missing required artifact "${deliverable.name}".`,
|
|
567
|
-
nodeId: params.node.id,
|
|
568
|
-
}),
|
|
569
|
-
)
|
|
570
|
-
}
|
|
571
|
-
continue
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
if (artifact.kind !== deliverable.kind) {
|
|
575
|
-
blocking.push(
|
|
576
|
-
createIssue({
|
|
577
|
-
code: 'artifact_kind_mismatch',
|
|
578
|
-
message: `Artifact "${deliverable.name}" must be of kind "${deliverable.kind}".`,
|
|
579
|
-
nodeId: params.node.id,
|
|
580
|
-
}),
|
|
581
|
-
)
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (deliverable.schemaRef) {
|
|
585
|
-
const artifactSchema = resolveSchemaRef(params.draft, deliverable.schemaRef)
|
|
586
|
-
if (!artifact.payload) {
|
|
587
|
-
blocking.push(
|
|
588
|
-
createIssue({
|
|
589
|
-
code: 'artifact_payload_missing',
|
|
590
|
-
message: `Artifact "${deliverable.name}" must include payload data for schema validation.`,
|
|
591
|
-
nodeId: params.node.id,
|
|
592
|
-
}),
|
|
593
|
-
)
|
|
594
|
-
} else if (artifactSchema) {
|
|
595
|
-
const schemaIssues = validateSchemaValue({
|
|
596
|
-
schema: artifactSchema,
|
|
597
|
-
value: artifact.payload,
|
|
598
|
-
path: `artifact:${deliverable.name}`,
|
|
599
|
-
})
|
|
600
|
-
if (schemaIssues.length > 0) {
|
|
601
|
-
blocking.push(
|
|
602
|
-
createIssue({
|
|
603
|
-
code: 'schema_validation_failed',
|
|
604
|
-
message: `Artifact "${deliverable.name}" failed schema validation.`,
|
|
605
|
-
nodeId: params.node.id,
|
|
606
|
-
detail: { issues: schemaIssues, artifact: deliverable.name },
|
|
607
|
-
}),
|
|
608
|
-
)
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
for (const check of params.node.completionChecks) {
|
|
615
|
-
const issue = this.evaluateCompletionCheck({
|
|
616
|
-
draft: params.draft,
|
|
617
|
-
node: params.node,
|
|
618
|
-
result: params.result,
|
|
619
|
-
check,
|
|
620
|
-
})
|
|
621
|
-
if (!issue) continue
|
|
622
|
-
if (issue.severity === 'warning') {
|
|
623
|
-
warnings.push(issue)
|
|
624
|
-
} else {
|
|
625
|
-
blocking.push(issue)
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
return { blocking, warnings, failureClass: this.resolveFailureClass(blocking) }
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
private evaluateCompletionCheck(params: {
|
|
633
|
-
draft: Pick<PlanDraft, 'schemas'>
|
|
634
|
-
node: PlanNodeValidationSpec
|
|
635
|
-
result: PlanNodeResultSubmission
|
|
636
|
-
check: PlanCompletionCheck
|
|
637
|
-
}): PlanValidationIssueInput | null {
|
|
638
|
-
const { check, node, result, draft } = params
|
|
639
|
-
const artifactName = typeof check.config.artifact === 'string' ? check.config.artifact : undefined
|
|
640
|
-
const artifact = artifactName ? result.artifacts.find((candidate) => candidate.name === artifactName) : undefined
|
|
641
|
-
const target =
|
|
642
|
-
artifactName && artifact
|
|
643
|
-
? artifact.payload
|
|
644
|
-
: typeof check.config.target === 'string'
|
|
645
|
-
? readPathValue(result.structuredOutput, check.config.target)
|
|
646
|
-
: result.structuredOutput
|
|
647
|
-
|
|
648
|
-
if (check.type === 'schema') {
|
|
649
|
-
const schemaRef =
|
|
650
|
-
(typeof check.config.schemaRef === 'string' ? check.config.schemaRef : undefined) ??
|
|
651
|
-
(artifactName ? node.deliverables.find((candidate) => candidate.name === artifactName)?.schemaRef : undefined)
|
|
652
|
-
if (!schemaRef) {
|
|
653
|
-
return createIssue({
|
|
654
|
-
code: 'schema_check_missing_schema_ref',
|
|
655
|
-
message: `Completion check "${check.description}" does not resolve a schema reference.`,
|
|
656
|
-
nodeId: node.id,
|
|
657
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
658
|
-
})
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
if (artifactName && !artifact) {
|
|
662
|
-
return createIssue({
|
|
663
|
-
code: 'schema_check_artifact_missing',
|
|
664
|
-
message: `Completion check "${check.description}" requires artifact "${artifactName}".`,
|
|
665
|
-
nodeId: node.id,
|
|
666
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
667
|
-
})
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
const schema = resolveSchemaRef(draft, schemaRef)
|
|
671
|
-
if (!schema) {
|
|
672
|
-
return createIssue({
|
|
673
|
-
code: 'schema_check_schema_missing',
|
|
674
|
-
message: `Completion check "${check.description}" references unknown schema "${schemaRef}".`,
|
|
675
|
-
nodeId: node.id,
|
|
676
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
677
|
-
})
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const schemaIssues = validateSchemaValue({
|
|
681
|
-
schema,
|
|
682
|
-
value: target,
|
|
683
|
-
path: artifactName ? `artifact:${artifactName}` : 'structuredOutput',
|
|
684
|
-
})
|
|
685
|
-
if (schemaIssues.length > 0) {
|
|
686
|
-
return createIssue({
|
|
687
|
-
code: 'schema_validation_failed',
|
|
688
|
-
message: `Completion check "${check.description}" failed schema validation.`,
|
|
689
|
-
nodeId: node.id,
|
|
690
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
691
|
-
detail: { issues: schemaIssues, schemaRef },
|
|
692
|
-
})
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
return null
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (check.type === 'assertion') {
|
|
699
|
-
const requiredFields = Array.isArray(check.config.mustContainFields)
|
|
700
|
-
? check.config.mustContainFields.filter((field): field is string => typeof field === 'string')
|
|
701
|
-
: []
|
|
702
|
-
if (requiredFields.length > 0 && !hasAllFields(target, requiredFields)) {
|
|
703
|
-
return createIssue({
|
|
704
|
-
code: 'assertion_failed',
|
|
705
|
-
message: `Assertion "${check.description}" failed because required fields are missing.`,
|
|
706
|
-
nodeId: node.id,
|
|
707
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
708
|
-
detail: { mustContainFields: requiredFields },
|
|
709
|
-
})
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const equalityPath = typeof check.config.path === 'string' ? check.config.path : undefined
|
|
713
|
-
if (equalityPath && 'equals' in check.config) {
|
|
714
|
-
const actual = readPathValue(target, equalityPath)
|
|
715
|
-
if (!Object.is(actual, check.config.equals)) {
|
|
716
|
-
return createIssue({
|
|
717
|
-
code: 'assertion_failed',
|
|
718
|
-
message: `Assertion "${check.description}" failed at path "${equalityPath}".`,
|
|
719
|
-
nodeId: node.id,
|
|
720
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
721
|
-
detail: { path: equalityPath, expected: check.config.equals, actual },
|
|
722
|
-
})
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const truthyPaths = Array.isArray(check.config.truthyPaths)
|
|
727
|
-
? check.config.truthyPaths.filter((entry): entry is string => typeof entry === 'string')
|
|
728
|
-
: []
|
|
729
|
-
if (truthyPaths.some((path) => !readPathValue(target, path))) {
|
|
730
|
-
return createIssue({
|
|
731
|
-
code: 'assertion_failed',
|
|
732
|
-
message: `Assertion "${check.description}" expected truthy values that were not present.`,
|
|
733
|
-
nodeId: node.id,
|
|
734
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
735
|
-
detail: { truthyPaths },
|
|
736
|
-
})
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
return null
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (check.type === 'tool-check') {
|
|
743
|
-
const mode = typeof check.config.mode === 'string' ? check.config.mode : 'artifact-present'
|
|
744
|
-
if (mode === 'artifact-present' && artifactName && !artifact) {
|
|
745
|
-
return createIssue({
|
|
746
|
-
code: 'required_artifact_missing',
|
|
747
|
-
message: `Tool check "${check.description}" requires artifact "${artifactName}".`,
|
|
748
|
-
nodeId: node.id,
|
|
749
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
750
|
-
})
|
|
751
|
-
}
|
|
752
|
-
if (
|
|
753
|
-
mode === 'artifact-kind' &&
|
|
754
|
-
artifactName &&
|
|
755
|
-
artifact &&
|
|
756
|
-
typeof check.config.kind === 'string' &&
|
|
757
|
-
artifact.kind !== check.config.kind
|
|
758
|
-
) {
|
|
759
|
-
return createIssue({
|
|
760
|
-
code: 'artifact_kind_mismatch',
|
|
761
|
-
message: `Tool check "${check.description}" expected artifact "${artifactName}" to be kind "${check.config.kind}".`,
|
|
762
|
-
nodeId: node.id,
|
|
763
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
764
|
-
})
|
|
765
|
-
}
|
|
766
|
-
if (
|
|
767
|
-
mode === 'min-artifact-count' &&
|
|
768
|
-
typeof check.config.count === 'number' &&
|
|
769
|
-
result.artifacts.length < check.config.count
|
|
770
|
-
) {
|
|
771
|
-
return createIssue({
|
|
772
|
-
code: 'required_artifact_missing',
|
|
773
|
-
message: `Tool check "${check.description}" expected at least ${check.config.count} artifact(s).`,
|
|
774
|
-
nodeId: node.id,
|
|
775
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
776
|
-
})
|
|
777
|
-
}
|
|
778
|
-
return null
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
if (check.type === 'human-approval') {
|
|
782
|
-
const approvedField = typeof check.config.approvedField === 'string' ? check.config.approvedField : 'approved'
|
|
783
|
-
if (readPathValue(result.structuredOutput, approvedField) !== true) {
|
|
784
|
-
return createIssue({
|
|
785
|
-
code: 'human_rejected',
|
|
786
|
-
message: `Human approval check "${check.description}" did not pass.`,
|
|
787
|
-
nodeId: node.id,
|
|
788
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
789
|
-
detail: { approvedField },
|
|
790
|
-
})
|
|
791
|
-
}
|
|
792
|
-
return null
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const resultField = typeof check.config.resultField === 'string' ? check.config.resultField : 'passed'
|
|
796
|
-
if (readPathValue(result.structuredOutput, resultField) !== true) {
|
|
797
|
-
return createIssue({
|
|
798
|
-
code: 'schema_validation_failed',
|
|
799
|
-
message: `LLM judge check "${check.description}" did not report success.`,
|
|
800
|
-
nodeId: node.id,
|
|
801
|
-
severity: check.blocking ? 'blocking' : 'warning',
|
|
802
|
-
detail: { resultField },
|
|
803
|
-
})
|
|
804
|
-
}
|
|
805
|
-
return null
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
private resolveFailureClass(blocking: PlanValidationIssueInput[]): PlanFailureClass | null {
|
|
809
|
-
for (const issue of blocking) {
|
|
810
|
-
if (issue.code === 'required_artifact_missing') return 'required_artifact_missing'
|
|
811
|
-
if (issue.code === 'human_rejected') return 'human_rejected'
|
|
812
|
-
if (issue.code === 'schema_validation_failed') return 'schema_validation_failed'
|
|
813
|
-
}
|
|
814
|
-
return blocking.length > 0 ? 'non_recoverable_logic_error' : null
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
export const planValidatorService = new PlanValidatorService()
|