diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index 736bb7d470d..c28a36e4511 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -677,6 +677,7 @@ export type { AgentRunState } from './schemas/agent-run-reducer'; export { formatDebugJson, summarizeJsonValue, + stepInstructions, parseSystemPromptForDisplay, parseMessageBlocks, parseUsageSummary, diff --git a/packages/@n8n/api-types/src/schemas/__tests__/llm-step-display.test.ts b/packages/@n8n/api-types/src/schemas/__tests__/llm-step-display.test.ts index 9cf33f1db3e..3529eddfc0e 100644 --- a/packages/@n8n/api-types/src/schemas/__tests__/llm-step-display.test.ts +++ b/packages/@n8n/api-types/src/schemas/__tests__/llm-step-display.test.ts @@ -12,6 +12,7 @@ import { parseUsageSummary, summarizeJsonValue, extractObservationsBlock, + stepInstructions, } from '../llm-step-display'; describe('llm-step-display', () => { @@ -287,4 +288,36 @@ describe('llm-step-display', () => { { type: 'text', text: 'Skill loading protocol' }, ]); }); + + describe('stepInstructions', () => { + it('reads the v7 `instructions` field', () => { + expect(stepInstructions({ instructions: 'You are helpful.' })).toBe('You are helpful.'); + }); + + it('falls back to the pre-v7 `system` field for older snapshots', () => { + expect(stepInstructions({ system: 'You are helpful.' })).toBe('You are helpful.'); + }); + + it('prefers `instructions` when a snapshot somehow carries both', () => { + expect(stepInstructions({ instructions: 'new', system: 'old' })).toBe('new'); + }); + + it('returns undefined for a missing input', () => { + expect(stepInstructions(undefined)).toBeUndefined(); + }); + + // The shape real captures carry: `{ role, content, providerOptions }`. + it('lets parseStepSummary size a v7 instructions object', () => { + const summary = parseStepSummary( + { instructions: { role: 'system', content: 'x'.repeat(120) } }, + { finishReason: 'stop' }, + ); + expect(summary.systemCharCount).toBe(120); + }); + + it('still sizes a pre-v7 string system prompt', () => { + const summary = parseStepSummary({ system: 'x'.repeat(42) }, { finishReason: 'stop' }); + expect(summary.systemCharCount).toBe(42); + }); + }); }); diff --git a/packages/@n8n/api-types/src/schemas/llm-step-display.ts b/packages/@n8n/api-types/src/schemas/llm-step-display.ts index 63c28da8046..2e2c41eb1a8 100644 --- a/packages/@n8n/api-types/src/schemas/llm-step-display.ts +++ b/packages/@n8n/api-types/src/schemas/llm-step-display.ts @@ -431,6 +431,16 @@ function extractObservationsFromBlock(block: ReadableContentBlock): { }; } +/** + * The step's system prompt, wherever the SDK put it. AI SDK v7 standardizes it as + * `instructions` (`StandardizedPrompt`); `system` is the pre-v7 name, kept as a + * fallback so snapshots captured before the rename still render. Without this the + * System section — and the `` block inside it — reads as empty. + */ +export function stepInstructions(input: Record | undefined): unknown { + return input?.instructions ?? input?.system; +} + export function parseSystemPromptForDisplay(system: unknown): ParsedSystemPromptDisplay { const blocks = parseSystemBlocks(system); const observationsParts: string[] = []; @@ -588,7 +598,15 @@ export function parseInputExtras(input: Record | undefined): un if (!input) return undefined; const extras: Record = {}; - const primaryKeys = new Set(['system', 'messages', 'stepNumber', 'sdkStepNumber']); + // `instructions` is the v7 name for `system` — both are rendered as the System + // section, so neither belongs in the extras dump. + const primaryKeys = new Set([ + 'system', + 'instructions', + 'messages', + 'stepNumber', + 'sdkStepNumber', + ]); for (const [key, value] of Object.entries(input)) { if (!primaryKeys.has(key)) { @@ -753,11 +771,15 @@ export function parseStepSummary( } } + // Same field move as the System section: read through `stepInstructions`, or + // this silently stays undefined for every v7 snapshot. `parseSystemBlocks` + // handles the string, array and single-object forms alike. + const instructions = stepInstructions(input); let systemCharCount: number | undefined; - if (typeof input?.system === 'string') { - systemCharCount = input.system.length; - } else if (Array.isArray(input?.system)) { - systemCharCount = parseSystemBlocks(input.system).reduce( + if (typeof instructions === 'string') { + systemCharCount = instructions.length; + } else if (instructions !== undefined && instructions !== null) { + systemCharCount = parseSystemBlocks(instructions).reduce( (total, block) => total + block.content.length, 0, ); diff --git a/packages/@n8n/instance-ai/evaluations/report/run-debug-report.ts b/packages/@n8n/instance-ai/evaluations/report/run-debug-report.ts index 88327996917..bd82ed083ec 100644 --- a/packages/@n8n/instance-ai/evaluations/report/run-debug-report.ts +++ b/packages/@n8n/instance-ai/evaluations/report/run-debug-report.ts @@ -21,6 +21,7 @@ import { parseStepSummary, parseSystemPromptForDisplay, parseUsageSummary, + stepInstructions, } from '@n8n/api-types'; import fs from 'fs'; import path from 'path'; @@ -116,7 +117,7 @@ function renderStepDetail( step: InstanceAiRunDebugStep, workflowCode: InstanceAiRunDebugWorkflowCodeSnapshot[], ): string { - const parsedSystem = parseSystemPromptForDisplay(step.input?.system); + const parsedSystem = parseSystemPromptForDisplay(stepInstructions(step.input)); const messageBlocks = parseMessageBlocks(step.input?.messages); const inputExtras = parseInputExtras(step.input); const outputBlocks = parseOutputDisplayBlocks(step.output); diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiLlmStepDetail.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiLlmStepDetail.vue index 6c898544fad..dd03285ce0b 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiLlmStepDetail.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiLlmStepDetail.vue @@ -8,6 +8,7 @@ import { parseOutputExtras, parseSystemPromptForDisplay, parseUsageSummary, + stepInstructions, } from '@n8n/api-types'; import { N8nText } from '@n8n/design-system'; import { useI18n } from '@n8n/i18n'; @@ -26,7 +27,9 @@ const props = defineProps<{ const i18n = useI18n(); const outputSectionRef = ref(null); -const parsedSystemPrompt = computed(() => parseSystemPromptForDisplay(props.input?.system)); +const parsedSystemPrompt = computed(() => + parseSystemPromptForDisplay(stepInstructions(props.input)), +); const systemBlocks = computed(() => parsedSystemPrompt.value.systemBlocks); const systemObservations = computed(() => parsedSystemPrompt.value.observations); const messageBlocks = computed(() => parseMessageBlocks(props.input?.messages));