// Agent personas — named, role-scoped sub-system prompts. // Each persona stacks on top of the main systemPrompt to specialize the // assistant for a task. Persisted in localStorage; reactive across tabs. import { useEffect, useSyncExternalStore } from "react" export type Agent = { id: string name: string role: string prompt: string } export const DEFAULT_AGENTS: Agent[] = [ { id: "generalist", name: "Atlas", role: "Generalist", prompt: "You handle anything: chat, planning, summaries, casual questions. Match the user's tone. Keep replies as long as the task deserves — terse for quick questions, detailed when explaining.", }, { id: "coder", name: "Forge", role: "Software engineer", prompt: "You are a senior software engineer. Write idiomatic, well-typed code. Prefer concrete examples over abstract advice. When asked to fix a bug, identify root cause before patching. Use markdown code blocks with language tags. Mention edge cases briefly when relevant.", }, { id: "writer", name: "Inkwell", role: "Writer", prompt: "You are a prose writer. Produce vivid, well-paced text — short stories, copy, emails, essays. Vary sentence length. Show, don't tell. When the user asks for a draft, deliver the draft, not a description of it.", }, { id: "researcher", name: "Pilot", role: "Researcher", prompt: "You are a careful researcher. Structure answers as: claim → evidence → caveat. Distinguish what is well-established from what is uncertain. Refuse to fabricate citations — if you don't know, say so.", }, { id: "ui-driver", name: "Cursor", role: "UI Operator", prompt: "You specialize in driving this app's UI on the user's behalf. Prefer doing over explaining. When the user asks for an action, emit an action block immediately. When they ask a question about the app, answer concisely and offer to do it.", }, ] const STORAGE_KEY = "crema.agents" const ACTIVE_KEY = "crema.assistant.activeAgent" const CHANGE_EVENT = "crema:agents-change" function isAgent(v: unknown): v is Agent { return ( !!v && typeof v === "object" && typeof (v as Agent).id === "string" && typeof (v as Agent).name === "string" && typeof (v as Agent).role === "string" && typeof (v as Agent).prompt === "string" ) } function readFromStorage(): Agent[] { if (typeof window === "undefined") return DEFAULT_AGENTS try { const raw = localStorage.getItem(STORAGE_KEY) if (!raw) return DEFAULT_AGENTS const parsed = JSON.parse(raw) if (!Array.isArray(parsed)) return DEFAULT_AGENTS const cleaned = parsed.filter(isAgent) return cleaned.length > 0 ? cleaned : DEFAULT_AGENTS } catch { return DEFAULT_AGENTS } } export function loadAgents(): Agent[] { return readFromStorage() } export function saveAgents(next: Agent[]) { if (typeof window === "undefined") return localStorage.setItem(STORAGE_KEY, JSON.stringify(next)) window.dispatchEvent(new CustomEvent(CHANGE_EVENT)) } export function resetAgents() { saveAgents(DEFAULT_AGENTS) } let cached: Agent[] | null = null function subscribe(cb: () => void): () => void { const onChange = () => { cached = null cb() } window.addEventListener(CHANGE_EVENT, onChange) window.addEventListener("storage", (e) => { if (e.key === STORAGE_KEY || e.key === ACTIVE_KEY) onChange() }) return () => { window.removeEventListener(CHANGE_EVENT, onChange) } } function getSnapshot(): Agent[] { if (!cached) cached = readFromStorage() return cached } function getServerSnapshot(): Agent[] { return DEFAULT_AGENTS } export function useAgents(): Agent[] { const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) useEffect(() => { cached = null }, []) return value } export function loadActiveAgentId(): string { if (typeof window === "undefined") return DEFAULT_AGENTS[0].id try { return localStorage.getItem(ACTIVE_KEY) ?? DEFAULT_AGENTS[0].id } catch { return DEFAULT_AGENTS[0].id } } export function saveActiveAgentId(id: string) { if (typeof window === "undefined") return localStorage.setItem(ACTIVE_KEY, id) window.dispatchEvent(new CustomEvent(CHANGE_EVENT)) } export function composeSystemPrompt( base: string, agent: Agent | undefined, ): string { if (!agent) return base return `${base}\n\nActive persona: ${agent.name} — ${agent.role}\n${agent.prompt}` } export function newAgentId(): string { return `agent-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}` }