diff --git a/packages/personality/src/index.ts b/packages/personality/src/index.ts index 44337565656e723c8b68bcfbc97828446acd6df6..f0a1dc49dde876abf86babf5158e361a4fd1d963 100644 --- a/packages/personality/src/index.ts +++ b/packages/personality/src/index.ts @@ -91,75 +91,67 @@ function wrapPersonality(content: string): string { } export default function (pi: ExtensionAPI) { + // "active" tracks what the user last picked (persisted to disk). + // "session" is locked at session_start and never changes — it + // controls what goes into the system prompt for this session. let activePersonality: string | null = null; - let personalityContent: string | null = null; - let isFirstTurn = true; - - /** Load personality by name. Returns true if found and loaded. */ - function loadPersonality(name: string): boolean { - const content = readPersonalityContent(name); - if (!content) return false; + let sessionPersonalityContent: string | null = null; + let pendingReminder: string | null = null; + /** Set the active personality name. Does NOT touch session prompt. */ + function setActive(name: string | null): void { activePersonality = name; - personalityContent = content; - return true; - } - - /** Clear active personality (both in-memory and on disk). */ - function clearPersonality(): void { - activePersonality = null; - personalityContent = null; - writeActivePersonality(null); - } - - /** Switch to a personality: load into memory and persist to disk. */ - function switchPersonality(name: string): boolean { - if (!loadPersonality(name)) return false; writeActivePersonality(name); - return true; } - // Restore persisted personality on session start + // Lock personality into the system prompt at session start. + // This content never changes for the lifetime of the session. pi.on("session_start", async (_event, ctx) => { const persisted = readActivePersonality(); - - // Determine whether this is a fresh session or a resumed one - const entries = ctx.sessionManager.getEntries(); - const hasMessages = entries.some((e) => e.type === "message" && e.message.role === "assistant"); - isFirstTurn = !hasMessages; + pendingReminder = null; + sessionPersonalityContent = null; if (persisted) { - if (loadPersonality(persisted)) { + const content = readPersonalityContent(persisted); + if (content) { + activePersonality = persisted; + sessionPersonalityContent = content; ctx.ui.notify(`Set personality to "${persisted}"`, "info"); } else { // Personality file was removed since last session - clearPersonality(); + setActive(null); } } }); - // Inject personality into the system prompt or as a reminder message. - // On the first turn (no prior messages), we modify the system prompt - // directly so the personality is baked into the initial cache. - // On subsequent turns, we inject a reminder message instead to avoid - // invalidating the inference cache. + // Append the session personality to the system prompt (ephemeral, + // never persisted). The content is identical every turn so the + // provider cache stays valid. + // + // Mid-session switches only fire a one-shot message — they never + // alter the system prompt. pi.on("before_agent_start", async (event) => { - if (!personalityContent) return undefined; + if (!sessionPersonalityContent && !pendingReminder) return undefined; - if (isFirstTurn) { - isFirstTurn = false; - return { - systemPrompt: event.systemPrompt + wrapPersonality(personalityContent), - }; + const result: { + systemPrompt?: string; + message?: { customType: string; content: string; display: boolean }; + } = {}; + + if (sessionPersonalityContent) { + result.systemPrompt = event.systemPrompt + wrapPersonality(sessionPersonalityContent); } - return { - message: { - customType: "system-reminder", - content: wrapPersonality(personalityContent), + if (pendingReminder) { + result.message = { + customType: "personality-switch", + content: pendingReminder, display: false, - }, - }; + }; + pendingReminder = null; + } + + return result; }); // Register /personality command @@ -180,7 +172,8 @@ export default function (pi: ExtensionAPI) { return; } const was = activePersonality; - clearPersonality(); + pendingReminder = `Personality "${was}" has been cleared. Revert to your default behaviour.`; + setActive(null); ctx.ui.notify(`Personality cleared (was: ${was})`, "info"); return; } @@ -192,7 +185,13 @@ export default function (pi: ExtensionAPI) { ctx.ui.notify(`Personality "${arg}" not found`, "error"); return; } - switchPersonality(arg); + const content = readPersonalityContent(arg); + if (!content) { + ctx.ui.notify(`Personality "${arg}" could not be read`, "error"); + return; + } + pendingReminder = wrapPersonality(content); + setActive(arg); ctx.ui.notify(`Set personality to "${arg}"`, "info"); return; } @@ -220,13 +219,20 @@ export default function (pi: ExtensionAPI) { ctx.ui.notify("No personality is active", "info"); } else { const was = activePersonality; - clearPersonality(); + pendingReminder = `Personality "${was}" has been cleared. Revert to your default behaviour.`; + setActive(null); ctx.ui.notify(`Personality cleared (was: ${was})`, "info"); } return; } - switchPersonality(picked); + const content = readPersonalityContent(picked); + if (!content) { + ctx.ui.notify(`Personality "${picked}" could not be read`, "error"); + return; + } + pendingReminder = wrapPersonality(content); + setActive(picked); ctx.ui.notify(`Set personality to "${picked}"`, "info"); }, });