From a85e07743ec8299ef6491017bb012cc45017efa7 Mon Sep 17 00:00:00 2001 From: Robin Braumann Date: Mon, 11 May 2026 12:02:24 +0200 Subject: [PATCH] refactor(agents): rename memory profile blocks --- .../runtime/__tests__/agent-runtime.test.ts | 4 +- .../runtime/__tests__/memory-profiles.test.ts | 38 +++++----- .../runtime/__tests__/message-list.test.ts | 28 +++---- .../__tests__/observational-cycle.test.ts | 10 ++- .../agents/src/runtime/memory-profiles.ts | 75 +++++++++---------- .../@n8n/agents/src/runtime/message-list.ts | 28 +++---- .../agents/src/runtime/observational-cycle.ts | 18 ++--- .../@n8n/agents/src/runtime/working-memory.ts | 2 +- packages/@n8n/agents/src/sdk/memory.ts | 2 +- .../agents/src/types/runtime/message-list.ts | 4 +- packages/@n8n/agents/src/types/sdk/memory.ts | 2 +- .../@n8n/agents/src/types/sdk/observation.ts | 4 +- 12 files changed, 107 insertions(+), 108 deletions(-) diff --git a/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts b/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts index 0044ac7f6fb..f3754499d7a 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/agent-runtime.test.ts @@ -544,7 +544,7 @@ describe('AgentRuntime — memory profiles', () => { streamText.mockReset(); }); - it('loads persona and user profiles into the system prompt', async () => { + it('loads agent and user profiles into the system prompt', async () => { const memory = new InMemoryMemory(); await memory.saveMemoryProfile( { scopeKind: 'agent', scopeId: 'agent-1' }, @@ -573,7 +573,7 @@ describe('AgentRuntime — memory profiles', () => { expect(prompt).toContain(''); expect(prompt).toContain('When debugging, ask for the exact version before suggesting fixes.'); expect(prompt).toContain('The user prefers concise answers.'); - expect(prompt.indexOf('')).toBeLessThan(prompt.indexOf('')); + expect(prompt.indexOf('')).toBeLessThan(prompt.indexOf('')); expect(prompt).not.toContain(''); }); }); diff --git a/packages/@n8n/agents/src/runtime/__tests__/memory-profiles.test.ts b/packages/@n8n/agents/src/runtime/__tests__/memory-profiles.test.ts index b91ca332a91..b5f3efd1d1c 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/memory-profiles.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/memory-profiles.test.ts @@ -46,18 +46,17 @@ describe('memory profiles', () => { const prompt = DEFAULT_MEMORY_PROFILE_UPDATE_PROMPT; for (const phrase of [ - 'User profile captures stable cross-session information', - ' is not task memory', + 'User-profile captures stable cross-session information', + ' is not task memory', 'must never be connected to the current objective of an agent', - 'communication preferences', - 'durable workflow preferences', + 'stable preferences about communication style, workflow, tools, environment, ownership, or domain context', + 'User-profile may include durable user preferences', 'If the information would stop being useful after the current task ends', - 'belongs in ', - 'does not belong in ', + 'describes the agent', + 'does not belong in ', 'Existing profile content is not authoritative', - 'Persona captures actionable behavioral directives', - 'imperative system-instruction-style directives', - 'concrete future behavior change', + 'Agent-profile captures durable facts about the agent', + 'durable persona or operating mode', ]) { expect(prompt).toContain(phrase); } @@ -71,7 +70,6 @@ describe('memory profiles', () => { 'next actions', 'temporary constraints', 'session objectives', - 'descriptive agent facts', 'storage/data-model facts', 'model names', 'schema facts', @@ -86,9 +84,9 @@ describe('memory profiles', () => { it('updates memory profiles from the profile updater path', async () => { generateText.mockResolvedValueOnce({ text: JSON.stringify({ - persona: + agentProfile: 'When discussing memory architecture, distinguish durable profile state from session objective state.', - user: 'The user prefers concise updates.', + userProfile: 'The user prefers concise updates.', }), }); @@ -122,7 +120,7 @@ describe('memory profiles', () => { }); expect(generateText).toHaveBeenCalledTimes(1); expect(generateText.mock.calls[0][0].system).toContain( - 'Persona captures actionable behavioral directives', + 'Agent-profile captures durable facts about the agent', ); expect(generateText.mock.calls[0][0].system).toContain( 'Assistant messages are supporting context', @@ -144,9 +142,9 @@ describe('memory profiles', () => { it('updates memory profiles from the turn pair even when no entries are accepted', async () => { generateText.mockResolvedValueOnce({ text: JSON.stringify({ - persona: + agentProfile: 'When users describe technical issues, ask for the specific n8n version before suggesting fixes.', - user: 'The user prefers responses without business framing or em dashes.', + userProfile: 'The user prefers responses without business framing or em dashes.', }), }); @@ -197,7 +195,7 @@ describe('memory profiles', () => { ).resolves.toBeNull(); }); - it('loads resource profiles shared across agents and persona profiles scoped to one agent', async () => { + it('loads resource profiles shared across agents and agent profiles scoped to one agent', async () => { const memory = new InMemoryMemory(); await memory.saveMemoryProfile( { scopeKind: 'resource', scopeId: 'user-1' }, @@ -222,12 +220,12 @@ describe('memory profiles', () => { }); expect(agentOne).toEqual({ - persona: 'This agent handles memory debugging.', - user: 'The user prefers concise answers.', + agentProfile: 'This agent handles memory debugging.', + userProfile: 'The user prefers concise answers.', }); expect(agentTwo).toEqual({ - persona: 'This other agent handles invoices.', - user: 'The user prefers concise answers.', + agentProfile: 'This other agent handles invoices.', + userProfile: 'The user prefers concise answers.', }); }); }); diff --git a/packages/@n8n/agents/src/runtime/__tests__/message-list.test.ts b/packages/@n8n/agents/src/runtime/__tests__/message-list.test.ts index aedfe07a409..5f79041e0c7 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/message-list.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/message-list.test.ts @@ -166,11 +166,11 @@ describe('AgentMessageList — forLlm working memory', () => { expect(prompt).not.toContain('Current template'); }); - it('renders persona, user, and session memory inside memory_blocks', () => { + it('renders agent profile, user profile, and session memory inside memory_blocks', () => { const list = new AgentMessageList(); list.memoryProfile = { - persona: 'This agent specializes in n8n memory work.', - user: 'The user prefers concise answers.', + agentProfile: 'This agent specializes in n8n memory work.', + userProfile: 'The user prefers concise answers.', }; list.workingMemory = { template: '# Thread memory', @@ -183,28 +183,28 @@ describe('AgentMessageList — forLlm working memory', () => { expect(prompt).toContain(''); expect(prompt).toContain( [ - '', - 'Durable behavior rules this agent should follow with this user.', + '', + 'Durable persona, role, and operating style for this agent.', '', 'This agent specializes in n8n memory work.', '', - '', + '', ].join('\n'), ); expect(prompt).toContain( [ - '', - 'Stable user preferences and context shared across agents.', + '', + 'Stable facts and preferences about the user or resource.', '', 'The user prefers concise answers.', '', - '', + '', ].join('\n'), ); expect(prompt).toContain(''); expect(prompt).toContain('Current objective: verify prompt sections.'); - expect(prompt.indexOf('')).toBeLessThan(prompt.indexOf('')); - expect(prompt.indexOf('')).toBeLessThan(prompt.indexOf('')); + expect(prompt.indexOf('')).toBeLessThan(prompt.indexOf('')); + expect(prompt.indexOf('')).toBeLessThan(prompt.indexOf('')); expect(prompt).not.toContain(''); }); @@ -306,13 +306,13 @@ describe('AgentMessageList — deserialize', () => { it('preserves injected profile context across serialization', () => { const list = new AgentMessageList(); - list.memoryProfile = { persona: 'Agent profile.', user: 'Resource profile.' }; + list.memoryProfile = { agentProfile: 'Agent profile.', userProfile: 'Resource profile.' }; const restored = AgentMessageList.deserialize(list.serialize()); expect(restored.memoryProfile).toEqual({ - persona: 'Agent profile.', - user: 'Resource profile.', + agentProfile: 'Agent profile.', + userProfile: 'Resource profile.', }); }); }); diff --git a/packages/@n8n/agents/src/runtime/__tests__/observational-cycle.test.ts b/packages/@n8n/agents/src/runtime/__tests__/observational-cycle.test.ts index 530e4d53cdd..9afa538908d 100644 --- a/packages/@n8n/agents/src/runtime/__tests__/observational-cycle.test.ts +++ b/packages/@n8n/agents/src/runtime/__tests__/observational-cycle.test.ts @@ -368,8 +368,8 @@ describe('runObservationalCycle', () => { opts(mem, { observe: undefined, memoryProfile: { - persona: 'This agent specializes in memory debugging.', - user: 'The user prefers concise answers.', + agentProfile: 'This agent specializes in memory debugging.', + userProfile: 'The user prefers concise answers.', }, }), ); @@ -377,9 +377,11 @@ describe('runObservationalCycle', () => { const call = mockGenerateText.mock.calls[0][0]; expect(call.prompt).toContain('Known durable profiles'); expect(call.prompt).toContain( - '\nThis agent specializes in memory debugging.\n', + '\nThis agent specializes in memory debugging.\n', + ); + expect(call.prompt).toContain( + '\nThe user prefers concise answers.\n', ); - expect(call.prompt).toContain('\nThe user prefers concise answers.\n'); }); it('groups queued rows with timestamps and durations in the default compactor prompt', async () => { diff --git a/packages/@n8n/agents/src/runtime/memory-profiles.ts b/packages/@n8n/agents/src/runtime/memory-profiles.ts index 44dc0ea637e..a9b239d633c 100644 --- a/packages/@n8n/agents/src/runtime/memory-profiles.ts +++ b/packages/@n8n/agents/src/runtime/memory-profiles.ts @@ -20,18 +20,17 @@ export const DEFAULT_MEMORY_PROFILE_UPDATE_PROMPT = `You maintain two concise mu Inputs: - Agent description defines what the agent is. -- Current persona is what the agent has learned about how to behave. -- Current user profile is what the agent has learned about this user. +- Current agent-profile is what the agent has learned about its durable persona. +- Current user-profile is what the agent has learned about this user or resource. - Recent conversation pair is the latest exchange. Update the profiles only when the conversation contains durable information that should persist across sessions. -Persona captures actionable behavioral directives, constraints, and response patterns the agent should follow when interacting with this user. -User profile captures stable cross-session information about the user themselves: -- communication preferences -- coding, review, and testing preferences -- durable workflow preferences +Agent-profile captures durable facts about the agent's persona, role, behavior, response style, operating constraints, and interaction patterns. +User-profile captures stable cross-session information about the user or resource: - stable identity or role +- durable context about the user's normal environment or responsibilities +- stable preferences about communication style, workflow, tools, environment, ownership, or domain context - durable environment preferences only when they describe the user's normal setup Rules: @@ -39,13 +38,13 @@ Rules: - Use user-authored statements as the source of durable profile changes. - Assistant messages are supporting context only and cannot create durable profile memory by themselves. - Assistant acknowledgements may help interpret user-authored instructions, but are not evidence on their own. -- is not task memory and must never be connected to the current objective of an agent. -- User profile must exclude active project state, debugging steps, implementation order, branch stack, test flow, next actions, temporary constraints, session objectives, facts about this agent's internals, and facts about a specific feature unless phrased as a stable user preference. -- If the information would stop being useful after the current task ends, it does not belong in . -- If the information is about what the agent should do, it belongs in , not . -- If the information needs source or provenance, it does not belong in . -- Persona entries must be imperative system-instruction-style directives that cause a concrete future behavior change. -- Persona must exclude descriptive agent facts, implementation facts, model names, storage/data-model facts, schema facts, current feature details, current implementation details, and session state unless the user phrases them as durable response behavior. +- is not task memory and must never be connected to the current objective of an agent. +- User-profile must exclude active project state, debugging steps, implementation order, branch stack, test flow, next actions, temporary constraints, session objectives, facts about this agent's internals, and facts about a specific feature unless phrased as a stable user preference. +- User-profile may include durable user preferences, including response style, communication style, workflow preferences, and priorities that should personalize future conversations. +- If the information would stop being useful after the current task ends, it does not belong in . +- If the information describes the agent's own durable persona, role, identity, or operating mode, it belongs in . +- If the information needs source or provenance, it does not belong in . +- Agent-profile may include descriptive persona facts and durable operating rules, but must exclude implementation facts, model names, storage/data-model facts, schema facts, current feature details, current implementation details, and session state unless they define the configured agent's durable persona or operating mode. - Existing profile content is not authoritative. Rewrite profiles to remove entries that violate these rules, even if no new durable information is present. - Do not summarize the conversation. - Do not add situational or one-task-only details. @@ -53,7 +52,7 @@ Rules: - If a profile needs no update or cleanup, return the existing profile content exactly. Return only JSON in this exact shape: -{"persona":"...","user":"..."}`; +{"agentProfile":"...","userProfile":"..."}`; interface NormalizedMemoryProfilesConfig { profileUpdatePrompt: string; @@ -123,8 +122,8 @@ export async function updateMemoryProfilesFromTurn(opts: { system: normalized.profileUpdatePrompt, prompt: renderMemoryProfileUpdatePrompt({ agentDescription: normalized.agentDescription, - persona: current?.persona ?? '', - user: current?.user ?? '', + agentProfile: current?.agentProfile ?? '', + userProfile: current?.userProfile ?? '', turn, }), }); @@ -135,14 +134,14 @@ export async function updateMemoryProfilesFromTurn(opts: { await saveProfileIfChanged({ memory: opts.memory, scope: agentMemoryProfileScope(opts.scope.agentId), - current: current?.persona ?? '', - next: parsed.persona, + current: current?.agentProfile ?? '', + next: parsed.agentProfile, }); await saveProfileIfChanged({ memory: opts.memory, scope: resourceMemoryProfileScope(opts.scope.resourceId), - current: current?.user ?? '', - next: parsed.user, + current: current?.userProfile ?? '', + next: parsed.userProfile, }); } catch (error) { opts.eventBus.emit({ @@ -159,7 +158,7 @@ async function loadMemoryProfiles( agentId: string | undefined, resourceId: string | undefined, ): Promise { - const [persona, user] = await Promise.all([ + const [agentProfile, userProfile] = await Promise.all([ agentId ? memory.getMemoryProfile(agentMemoryProfileScope(agentId)) : Promise.resolve(null), resourceId ? memory.getMemoryProfile(resourceMemoryProfileScope(resourceId)) @@ -167,16 +166,16 @@ async function loadMemoryProfiles( ]); const context = { - persona: persona?.content ?? null, - user: user?.content ?? null, + agentProfile: agentProfile?.content ?? null, + userProfile: userProfile?.content ?? null, }; - return context.persona || context.user ? context : undefined; + return context.agentProfile || context.userProfile ? context : undefined; } function renderMemoryProfileUpdatePrompt(ctx: { agentDescription?: string; - persona: string; - user: string; + agentProfile: string; + userProfile: string; turn: ProfileUpdateTurn; }): string { const agentDescription = ctx.agentDescription?.trim(); @@ -184,13 +183,13 @@ function renderMemoryProfileUpdatePrompt(ctx: { ...(agentDescription ? ['', agentDescription, '', ''] : []), - '', - ctx.persona.trim(), - '', + '', + ctx.agentProfile.trim(), + '', '', - '', - ctx.user.trim(), - '', + '', + ctx.userProfile.trim(), + '', '', '', '', @@ -204,13 +203,13 @@ function renderMemoryProfileUpdatePrompt(ctx: { ].join('\n'); } -function parseProfileUpdate(text: string): { persona: string; user: string } | null { +function parseProfileUpdate(text: string): { agentProfile: string; userProfile: string } | null { const parsed = parseJsonObject(stripMarkdownFence(text)); if (!isRecord(parsed)) return null; - const persona = parsed.persona; - const user = parsed.user; - if (typeof persona !== 'string' || typeof user !== 'string') return null; - return { persona: persona.trim(), user: user.trim() }; + const agentProfile = parsed.agentProfile; + const userProfile = parsed.userProfile; + if (typeof agentProfile !== 'string' || typeof userProfile !== 'string') return null; + return { agentProfile: agentProfile.trim(), userProfile: userProfile.trim() }; } async function saveProfileIfChanged(opts: { diff --git a/packages/@n8n/agents/src/runtime/message-list.ts b/packages/@n8n/agents/src/runtime/message-list.ts index 2565aa7fd74..c49155a128d 100644 --- a/packages/@n8n/agents/src/runtime/message-list.ts +++ b/packages/@n8n/agents/src/runtime/message-list.ts @@ -24,8 +24,8 @@ export interface WorkingMemoryContext { } export interface MemoryProfileContext { - persona?: string | null; - user?: string | null; + agentProfile?: string | null; + userProfile?: string | null; } /** @@ -223,30 +223,30 @@ export class AgentMessageList { let systemPrompt = baseInstructions; const memoryBlocks: string[] = []; - const persona = this.memoryProfile?.persona?.trim(); - if (persona) { + const agentProfile = this.memoryProfile?.agentProfile?.trim(); + if (agentProfile) { memoryBlocks.push( [ - '', - 'Durable behavior rules this agent should follow with this user.', + '', + 'Durable persona, role, and operating style for this agent.', '', - persona, + agentProfile, '', - '', + '', ].join('\n'), ); } - const user = this.memoryProfile?.user?.trim(); - if (user) { + const userProfile = this.memoryProfile?.userProfile?.trim(); + if (userProfile) { memoryBlocks.push( [ - '', - 'Stable user preferences and context shared across agents.', + '', + 'Stable facts and preferences about the user or resource.', '', - user, + userProfile, '', - '', + '', ].join('\n'), ); } diff --git a/packages/@n8n/agents/src/runtime/observational-cycle.ts b/packages/@n8n/agents/src/runtime/observational-cycle.ts index 23370b66d85..458f251a094 100644 --- a/packages/@n8n/agents/src/runtime/observational-cycle.ts +++ b/packages/@n8n/agents/src/runtime/observational-cycle.ts @@ -53,7 +53,7 @@ Evidence rules: - Transcript roles matter. User messages are authoritative for requested work, current-session goals, constraints, corrections, and decisions. - Assistant messages are supporting context only. A normal assistant reply is not verification evidence. -- Known persona and resource/user profiles are durable memory. Do not copy them +- Known agent and resource/user profiles are durable memory. Do not copy them into thread working memory unless the live transcript adds session-specific objective or task state. - Do not record assistant-created checklists, diagnostic questions, file/table @@ -65,9 +65,9 @@ Evidence rules: Rules: - Prefer explicit session state over broad durable profile facts. - Do not record stable user identity, general communication style, long-lived - user preferences, or durable agent/persona facts as thread working memory. + user-profile preferences, or durable agent-profile content as thread working memory. - Do not record general user preferences, repo-wide habits, style preferences, - or persona facts as session memory. + or agent-profile content as session memory. - If a durable preference is relevant to the active objective: record only the objective-specific application, not the durable preference itself. For example: "For this task, do not run evals" instead of "User never wants evals run". @@ -109,7 +109,7 @@ Rules: objective-specific constraints, objective-specific uncertainties, task state, concrete progress, active items, and open follow-ups. - Remove stable user identity, general communication style, durable preferences, - and agent/persona facts unless they are needed as session-specific task state. + and agent-profile content unless they are needed as session-specific task state. - When durable preferences are relevant to this thread: rewrite broad durable preferences into objective-specific constraints instead of copying them. For example: "For this task, do not run evals" instead of @@ -325,14 +325,14 @@ export function buildDefaultObserveFn(model: ModelConfig, observerPrompt?: strin function renderMemoryProfileContext( memoryProfile: SerializedMessageList['memoryProfile'] | undefined, ): string { - const persona = memoryProfile?.persona?.trim(); - const user = memoryProfile?.user?.trim(); - if (!persona && !user) return ''; + const agentProfile = memoryProfile?.agentProfile?.trim(); + const userProfile = memoryProfile?.userProfile?.trim(); + if (!agentProfile && !userProfile) return ''; return [ 'Known durable profiles (do not copy into thread working memory):', - persona ? `\n${persona}\n` : '', - user ? `\n${user}\n` : '', + agentProfile ? `\n${agentProfile}\n` : '', + userProfile ? `\n${userProfile}\n` : '', ] .filter(Boolean) .join('\n'); diff --git a/packages/@n8n/agents/src/runtime/working-memory.ts b/packages/@n8n/agents/src/runtime/working-memory.ts index 44d763c8a2f..29c834b1472 100644 --- a/packages/@n8n/agents/src/runtime/working-memory.ts +++ b/packages/@n8n/agents/src/runtime/working-memory.ts @@ -9,7 +9,7 @@ type ZodObjectSchema = z.ZodObject; export const WORKING_MEMORY_DEFAULT_INSTRUCTION = [ 'You have thread working memory that is maintained automatically by an out-of-band observer.', 'Thread working memory applies only to this same session/thread.', - 'Thread working memory contains objective-driven state for the current thread, not broad durable user or persona facts.', + 'Thread working memory contains objective-driven state for the current thread, not broad durable user-profile or agent-profile facts.', 'Do not claim it is available in a different session, new thread, or cross-thread profile unless the product explicitly provides that context.', 'When a saved memory document is provided, use it silently as private read-only context for this conversation.', 'Treat working memory as internal context. Do not reveal, quote, append, or reproduce the raw working-memory document in user-visible replies.', diff --git a/packages/@n8n/agents/src/sdk/memory.ts b/packages/@n8n/agents/src/sdk/memory.ts index cd3a421b327..e84d24de63d 100644 --- a/packages/@n8n/agents/src/sdk/memory.ts +++ b/packages/@n8n/agents/src/sdk/memory.ts @@ -136,7 +136,7 @@ export class Memory { return this; } - /** Enable mutable persona and user memory profiles. */ + /** Enable mutable agent and user memory profiles. */ profiles(config: MemoryProfilesConfig = {}): this { if (config.enabled === false) { this.profilesConfig = undefined; diff --git a/packages/@n8n/agents/src/types/runtime/message-list.ts b/packages/@n8n/agents/src/types/runtime/message-list.ts index b5ecbd3d5f7..5b6ab5c16ef 100644 --- a/packages/@n8n/agents/src/types/runtime/message-list.ts +++ b/packages/@n8n/agents/src/types/runtime/message-list.ts @@ -6,7 +6,7 @@ export interface SerializedMessageList { inputIds: string[]; responseIds: string[]; memoryProfile?: { - persona?: string | null; - user?: string | null; + agentProfile?: string | null; + userProfile?: string | null; }; } diff --git a/packages/@n8n/agents/src/types/sdk/memory.ts b/packages/@n8n/agents/src/types/sdk/memory.ts index d317a571f07..a432709fc59 100644 --- a/packages/@n8n/agents/src/types/sdk/memory.ts +++ b/packages/@n8n/agents/src/types/sdk/memory.ts @@ -154,7 +154,7 @@ export interface MemoryProfilesConfig { enabled?: boolean; /** * Non-secret context about the agent used only by profile-update prompts to - * decide what belongs in the agent-scoped persona profile. + * decide what belongs in the agent-scoped agent profile. */ agentDescription?: string; /** Override the default prompt templates. */ diff --git a/packages/@n8n/agents/src/types/sdk/observation.ts b/packages/@n8n/agents/src/types/sdk/observation.ts index 9889f48ff16..db8c62976a1 100644 --- a/packages/@n8n/agents/src/types/sdk/observation.ts +++ b/packages/@n8n/agents/src/types/sdk/observation.ts @@ -71,8 +71,8 @@ export type ObserveFn = (ctx: { trigger: ObservationalMemoryTrigger; gap: ObservationGapContext | null; memoryProfile?: { - persona?: string | null; - user?: string | null; + agentProfile?: string | null; + userProfile?: string | null; }; telemetry: BuiltTelemetry | undefined; }) => Promise;