refactor(agents): rename memory profile blocks

This commit is contained in:
Robin Braumann 2026-05-11 12:02:24 +02:00
parent 8e0a28dd5b
commit a85e07743e
12 changed files with 107 additions and 108 deletions

View File

@ -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('<memory_blocks>');
expect(prompt).toContain('When debugging, ask for the exact version before suggesting fixes.');
expect(prompt).toContain('The user prefers concise answers.');
expect(prompt.indexOf('<persona>')).toBeLessThan(prompt.indexOf('<user>'));
expect(prompt.indexOf('<agent-profile>')).toBeLessThan(prompt.indexOf('<user-profile>'));
expect(prompt).not.toContain('<memory>');
});
});

View File

@ -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',
'<user> is not task memory',
'User-profile captures stable cross-session information',
'<user-profile> 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 <persona>',
'does not belong in <user>',
'describes the agent',
'does not belong in <user-profile>',
'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.',
});
});
});

View File

@ -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('<memory_blocks>');
expect(prompt).toContain(
[
'<persona>',
'<description>Durable behavior rules this agent should follow with this user.</description>',
'<agent-profile>',
'<description>Durable persona, role, and operating style for this agent.</description>',
'<value>',
'This agent specializes in n8n memory work.',
'</value>',
'</persona>',
'</agent-profile>',
].join('\n'),
);
expect(prompt).toContain(
[
'<user>',
'<description>Stable user preferences and context shared across agents.</description>',
'<user-profile>',
'<description>Stable facts and preferences about the user or resource.</description>',
'<value>',
'The user prefers concise answers.',
'</value>',
'</user>',
'</user-profile>',
].join('\n'),
);
expect(prompt).toContain('<session-memory>');
expect(prompt).toContain('Current objective: verify prompt sections.');
expect(prompt.indexOf('<persona>')).toBeLessThan(prompt.indexOf('<user>'));
expect(prompt.indexOf('<user>')).toBeLessThan(prompt.indexOf('<session-memory>'));
expect(prompt.indexOf('<agent-profile>')).toBeLessThan(prompt.indexOf('<user-profile>'));
expect(prompt.indexOf('<user-profile>')).toBeLessThan(prompt.indexOf('<session-memory>'));
expect(prompt).not.toContain('<memory>');
});
@ -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.',
});
});
});

View File

@ -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(
'<persona>\nThis agent specializes in memory debugging.\n</persona>',
'<agent-profile>\nThis agent specializes in memory debugging.\n</agent-profile>',
);
expect(call.prompt).toContain(
'<user-profile>\nThe user prefers concise answers.\n</user-profile>',
);
expect(call.prompt).toContain('<user>\nThe user prefers concise answers.\n</user>');
});
it('groups queued rows with timestamps and durations in the default compactor prompt', async () => {

View File

@ -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.
- <user> 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 <user>.
- If the information is about what the agent should do, it belongs in <persona>, not <user>.
- If the information needs source or provenance, it does not belong in <user>.
- 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.
- <user-profile> 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 <user-profile>.
- If the information describes the agent's own durable persona, role, identity, or operating mode, it belongs in <agent-profile>.
- If the information needs source or provenance, it does not belong in <user-profile>.
- 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<SerializedMessageList['memoryProfile'] | undefined> {
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
? ['<agent-description>', agentDescription, '</agent-description>', '']
: []),
'<persona>',
ctx.persona.trim(),
'</persona>',
'<agent-profile>',
ctx.agentProfile.trim(),
'</agent-profile>',
'',
'<user>',
ctx.user.trim(),
'</user>',
'<user-profile>',
ctx.userProfile.trim(),
'</user-profile>',
'',
'<turn>',
'<user-message>',
@ -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: {

View File

@ -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(
[
'<persona>',
'<description>Durable behavior rules this agent should follow with this user.</description>',
'<agent-profile>',
'<description>Durable persona, role, and operating style for this agent.</description>',
'<value>',
persona,
agentProfile,
'</value>',
'</persona>',
'</agent-profile>',
].join('\n'),
);
}
const user = this.memoryProfile?.user?.trim();
if (user) {
const userProfile = this.memoryProfile?.userProfile?.trim();
if (userProfile) {
memoryBlocks.push(
[
'<user>',
'<description>Stable user preferences and context shared across agents.</description>',
'<user-profile>',
'<description>Stable facts and preferences about the user or resource.</description>',
'<value>',
user,
userProfile,
'</value>',
'</user>',
'</user-profile>',
].join('\n'),
);
}

View File

@ -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 ? `<persona>\n${persona}\n</persona>` : '',
user ? `<user>\n${user}\n</user>` : '',
agentProfile ? `<agent-profile>\n${agentProfile}\n</agent-profile>` : '',
userProfile ? `<user-profile>\n${userProfile}\n</user-profile>` : '',
]
.filter(Boolean)
.join('\n');

View File

@ -9,7 +9,7 @@ type ZodObjectSchema = z.ZodObject<z.ZodRawShape>;
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.',

View File

@ -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;

View File

@ -6,7 +6,7 @@ export interface SerializedMessageList {
inputIds: string[];
responseIds: string[];
memoryProfile?: {
persona?: string | null;
user?: string | null;
agentProfile?: string | null;
userProfile?: string | null;
};
}

View File

@ -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. */

View File

@ -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<NewObservation[]>;