diff --git a/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts b/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts index 96579e930..436fd4a57 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/workflow-change.ts @@ -309,8 +309,43 @@ function stableUnique(values: string[]): string[] { function normalizeAcceptance(assertions: ChangeSpec['acceptance'] | undefined): string[] { if (!Array.isArray(assertions)) return [] + const toCanonicalAssertion = ( + item: string | { kind?: string; assert: string } | undefined + ): string | null => { + if (!item) return null + const rawAssert = typeof item === 'string' ? item : item.assert + if (typeof rawAssert !== 'string' || rawAssert.trim().length === 0) return null + const assert = rawAssert.trim() + const kind = typeof item === 'string' ? '' : String(item.kind || '').trim().toLowerCase() + + const normalizeKnown = (value: string): string => { + if ( + value.startsWith('block_exists:') || + value.startsWith('path_exists:') || + value.startsWith('trigger_exists:') + ) { + return value + } + return '' + } + + const known = normalizeKnown(assert) + if (known) return known + + if (kind === 'block_exists') return `block_exists:${assert}` + if (kind === 'path_exists') return `path_exists:${assert}` + if (kind === 'trigger_exists') return `trigger_exists:${assert}` + + // Shorthand compatibility: if assertion looks like A->B, treat as path_exists. + if (assert.includes('->')) { + return `path_exists:${assert}` + } + // Single token fallback defaults to block_exists. + return `block_exists:${assert}` + } + return assertions - .map((item) => (typeof item === 'string' ? item : item?.assert)) + .map((item) => (typeof item === 'string' ? toCanonicalAssertion(item) : toCanonicalAssertion(item))) .filter((item): item is string => typeof item === 'string' && item.trim().length > 0) } @@ -721,6 +756,7 @@ async function compileChangeSpec(params: { const connectionState = buildConnectionState(workingState) const connectionTouchedSources = new Set() const plannedBlockTypes = new Map() + const schemaFallbackLogged = new Set() const isSchemaLoaded = (blockType: string | null): boolean => Boolean(blockType && schemaContext.loadedSchemaTypes.has(blockType)) @@ -737,15 +773,27 @@ async function compileChangeSpec(params: { if (isSchemaLoaded(blockType)) { return true } + // Intelligence-first fallback: compiler can still validate against registry schema + // even when context pack did not include that type. + if (getBlock(blockType)) { + if (!schemaFallbackLogged.has(blockType)) { + schemaFallbackLogged.add(blockType) + warnings.push( + `${operationName} on ${targetId} used server schema for block type "${blockType}" ` + + `(not present in context pack).` + ) + } + return true + } if (schemaContext.contextPackProvided) { diagnostics.push( - `${operationName} on ${targetId} requires schema for block type "${blockType}". ` + + `${operationName} on ${targetId} failed: unknown schema for block type "${blockType}". ` + `Call workflow_context_expand with blockTypes:["${blockType}"] and retry dry_run.` ) return false } diagnostics.push( - `${operationName} on ${targetId} requires schema-loaded context. ` + + `${operationName} on ${targetId} failed: unknown schema for block type "${blockType}". ` + `Call workflow_context_get, then workflow_context_expand for "${blockType}", ` + `then retry dry_run with contextPackId.` ) @@ -992,6 +1040,16 @@ async function compileChangeSpec(params: { } const topLevelField = pathSegments[0] if (!['name', 'type', 'triggerMode', 'advancedMode', 'enabled'].includes(topLevelField)) { + if ( + blockType === 'agent' && + ['systemPrompt', 'context', 'prompt', 'instructions', 'userPrompt'].includes(topLevelField) + ) { + diagnostics.push( + `Unsupported agent field "${change.path}" on ${targetId}. ` + + `Agent prompt configuration belongs in inputs.messages (messages-input), not top-level fields.` + ) + return + } diagnostics.push(`Unsupported top-level path "${change.path}" on ${targetId}`) return } diff --git a/apps/sim/lib/copilot/tools/server/workflow/workflow-verify.ts b/apps/sim/lib/copilot/tools/server/workflow/workflow-verify.ts index ce614b5b1..a9adb7091 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/workflow-verify.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/workflow-verify.ts @@ -79,8 +79,9 @@ function evaluateAssertions(params: { edges: Array> } assertions: string[] -}): { failures: string[]; checks: Array> } { +}): { failures: string[]; warnings: string[]; checks: Array> } { const failures: string[] = [] + const warnings: string[] = [] const checks: Array> = [] for (const assertion of params.assertions) { @@ -126,12 +127,13 @@ function evaluateAssertions(params: { continue } - // Unknown assertion format - mark as warning failure for explicit visibility. + // Unknown assertion format should not fail structural verification. + // Keep explicit visibility via warnings/check metadata. checks.push({ assert: assertion, passed: false, reason: 'unknown_assertion_type' }) - failures.push(`Unknown assertion format: ${assertion}`) + warnings.push(`Unknown assertion format: ${assertion}`) } - return { failures, checks } + return { failures, warnings, checks } } export const workflowVerifyServerTool: BaseServerTool = { @@ -182,6 +184,7 @@ export const workflowVerifyServerTool: BaseServerTool errorCount: validation.errors.length, warningCount: validation.warnings.length, assertionFailures: assertionResults.failures.length, + assertionWarnings: assertionResults.warnings.length, }) return { @@ -195,6 +198,7 @@ export const workflowVerifyServerTool: BaseServerTool }, assertions: assertionResults.checks, failures: assertionResults.failures, + warnings: assertionResults.warnings, } }, }