@wordbricks/persona is a persona memory and runtime library for LLM agents. It gives an application a durable memory substrate, a turn planner, prompt context assembly, and post-response memory review without owning your database connection, model provider, embedder, chat stack, or deployment runtime.
The memory model is brain-inspired rather than a single global vector search. Tonic identity memory - beliefs, habits, and style - has independent selection budgets from phasic episodic/source memory, so stable identity does not get crowded out by recent events. Recall combines lexical overlap with pgvector cosine similarity, then applies one-hop spreading activation across linked memories. Episodic availability follows an exponential forgetting curve, but recent activation and emotional salience can strengthen recall. Mood is represented as PAD (valence, arousal, dominance) and decays between turns. Beliefs are reconsolidated through reinforcement and contradiction, not overwritten. During generation, an agent can perform re-entrant recall by calling back into memory with a specific cue.
See ARCHITECTURE.md for the retrieval, forgetting, mood, and reconsolidation details.
This package is bring-your-own infrastructure:
- Bring your own Postgres database with pgvector. The schema is implemented with Drizzle and exported from
@wordbricks/persona/schema. - Bring your own Drizzle database handle.
drizzle-ormis a peer dependency so your app and this package share one Drizzle instance. - Bring your own LLM. Planning, triage, and consolidation use the
PersonaJsonLlmcallback and provide the workflow's strict Zodschemafor structured generation. - Bring your own embedder. Retrieval works lexically without embeddings, or semantically with any
PersonaEmbedder; the package includescreateOpenAiPersonaEmbedderfor OpenAI embeddings. - Bring your own chat runtime.
buildPersonaInstructionsreturns instructions that you pass to your normal agent or chat-completion stack. - Optionally attach an external memory service. The Hindsight adapter can recall, retain, and reflect through
createHindsightPersonaMemoryClient. - Use
PersonaLoggeranddeferhooks for serverless runtimes. In Cloudflare Workers, passdefer: ctx.waitUntilso background retain/review work can finish after the response.
- Node.js or Bun
- Postgres with pgvector enabled:
CREATE EXTENSION IF NOT EXISTS vector;Install the package and the shared Drizzle peer:
npm i @wordbricks/persona drizzle-ormYou will also need your normal Drizzle database driver and migration tooling, for example postgres and drizzle-kit.
Re-export the persona schema from your app and include that file in your drizzle-kit config. This lets your app own migrations while keeping table definitions sourced from the package.
// src/db/persona-schema.ts
export * from "@wordbricks/persona/schema";// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
out: "./drizzle",
schema: ["./src/db/schema.ts", "./src/db/persona-schema.ts"],
});Then generate and apply migrations through your normal Drizzle workflow.
tenantId, userId, chatSessionId, and chatMessageId are opaque string scoping columns. The package intentionally does not declare foreign keys to host-app tables. If your app wants referential constraints, add them in your own migrations.
Version 0.2.0 is a breaking release that replaces organization-scoped naming with tenant-scoped naming. Before upgrading the package in an existing app:
- Replace
organizationId,sourceOrganizationId, andtargetOrganizationIdwith theirtenantIdequivalents in application code, queued jobs, cached payloads, and serialized events. - Generate and review a migration that renames all persona table
organization_idcolumns totenant_id. Ensure the migration uses column renames instead of dropping and recreating columns. - Change existing
persona_scope = 'organization'rows and the column default topersona_scope = 'tenant'. - Rename the affected profile and alias indexes from
orgtotenant, or let the reviewed migration recreate them safely. - Update Hindsight tag consumers from
org_*totenant_*. Bank IDs remain stable when the underlying tenant identifier value is unchanged. - Deploy the database migration and all package consumers as one coordinated
release. Old application versions expect
organization_id, while 0.2.0 expectstenant_id, so mixed-version operation is not supported.
See CHANGELOG.md for the affected tables and a migration checklist.
Embeddings are stored at PERSONA_EMBEDDING_DIMENSION (1536), matching OpenAI text-embedding-3-small through the built-in helper. If you use a different embedder, keep the schema dimension aligned with that embedder.
This is the full wiring shape. The same flow is typechecked in examples/basic.ts.
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import {
buildPersonaInstructions,
createOpenAiPersonaEmbedder,
ingestPersonaSourceDocument,
preparePersonaRuntimeContext,
processPersonaMemoryConsolidationTick,
recordPostResponsePersonaMemoryReview,
rememberPersonaLayerMemory,
upsertPersonaProfile,
} from "@wordbricks/persona";
import type { PersonaDatabase, PersonaJsonLlm } from "@wordbricks/persona";
import * as personaSchema from "@wordbricks/persona/schema";
import { z } from "zod";
const sql = postgres(process.env.DATABASE_URL!);
const db = drizzle(sql, { schema: personaSchema }) as PersonaDatabase;
const tenantId = "tenant_123";
const personaKey = "product-coach";
const userId = "user_123";
const openAiApiKey = process.env.OPENAI_API_KEY!;
const embed = createOpenAiPersonaEmbedder(openAiApiKey);
const personaJsonLlm: PersonaJsonLlm = async ({
schema,
systemPrompt,
userPrompt,
workflow,
}) => {
const response = await fetch("https://api.example.com/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.JSON_LLM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "your-json-mode-model",
response_format: {
type: "json_schema",
json_schema: {
name: workflow,
schema: z.toJSONSchema(schema),
strict: true,
},
},
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: `${userPrompt}\n\nReturn JSON only.` },
],
temperature: 0,
}),
});
const payload = (await response.json()) as {
choices?: Array<{ message?: { content?: string | null } }>;
};
const content = payload.choices?.[0]?.message?.content;
if (!content) throw new Error("Persona JSON LLM returned no content.");
return JSON.parse(content) as unknown;
};
const persona = await upsertPersonaProfile(db, {
tenantId,
personaKey,
displayName: "Product Coach",
personaType: "synthetic_role",
consentStatus: "fictional_or_authorized",
policy: {
allowedUse: ["Help teams reason about product decisions."],
forbiddenUse: ["Do not present this persona as a real person."],
transparencyLabel: "AI persona simulation for Product Coach.",
},
profile: {
voice: "plain-spoken, rigorous, and concrete",
},
updatedByUserId: userId,
});
await ingestPersonaSourceDocument(db, {
tenantId,
personaKey,
title: "Product Coach Seed Notes",
rawText:
"The Product Coach prefers writing down the user problem, the bet, and the fastest falsifying signal before committing engineering time.",
sourceType: "seed",
sourcePriority: "synthetic",
rightsStatus: "owned",
embed,
});
await rememberPersonaLayerMemory(db, {
tenantId,
personaKey,
userId,
updatedByUserId: userId,
memoryKind: "habit",
title: "Product review habit",
summary:
"When reviewing product ideas, the persona asks for the riskiest assumption and the smallest credible test.",
content: {
triggerDescription: "When asked to review a product idea",
defaultResponsePattern: [
"Name the assumption, name the user evidence, then suggest the smallest test.",
],
},
embed,
});
const userMessage = "Should we build a dashboard for this feature first?";
const runtime = await preparePersonaRuntimeContext(db, {
tenantId,
personaKey,
userId,
message: userMessage,
disclosurePolicy: "always",
llm: personaJsonLlm,
embed,
});
const systemPrompt = buildPersonaInstructions({
personaKey,
language: "en",
disclosurePolicy: runtime.disclosurePolicy,
personaPromptContext: runtime.promptContext,
turnPlan: runtime.turnPlan,
});
const answer = await yourChatLlm({
system: systemPrompt,
user: userMessage,
});
await recordPostResponsePersonaMemoryReview(db, {
tenantId,
userId,
userMessage,
assistantMessage: answer,
persona: runtime.persona,
turnPlan: runtime.turnPlan,
workspaceId: runtime.workspaceId,
llm: personaJsonLlm,
});
// Run this from a cron or queue worker.
await processPersonaMemoryConsolidationTick({
db,
consolidate: personaJsonLlm,
embed,
});| Area | Module | Key exports |
|---|---|---|
| Profile | @wordbricks/persona |
upsertPersonaProfile, loadPersonaProfile, publishPersonaProfile, copyPersonaProfile, deletePersonaProfile, upsertPersonaAlias, listPersonaAliases |
| Source ingestion | @wordbricks/persona |
ingestPersonaSourceDocument, chunkPersonaSourceText, draftPersonaMemoriesFromSourceDocument, activatePersonaDraftMemory, rememberPersonaLayerMemory, forgetPersonaLayerMemory |
| Runtime and turn memory | @wordbricks/persona |
preparePersonaRuntimeContext, planPersonaTurnWithLlm, recallPersonaMemoriesForCue, recordPostResponsePersonaMemoryReview, triagePostResponseInteractionMemoryWithLlm, processPersonaMemoryConsolidationTick |
| Mood and selection | @wordbricks/persona |
calculatePersonaMoodUpdate, estimateTurnAffect, updatePersonaMood, selectPersonaMemories, selectPersonaMemoriesWithScores, calculateMemoryAvailability |
| Embeddings | @wordbricks/persona |
createOpenAiPersonaEmbedder, upsertPersonaMemoryEmbeddings, backfillPersonaMemoryEmbeddings, hashPersonaMemoryText, normalizePersonaEmbeddingText |
| Hindsight adapter | @wordbricks/persona |
createHindsightPersonaMemoryConfig, createHindsightPersonaMemoryClient, createNoopHindsightPersonaMemoryClient, hindsightPersonaBankId, hindsightPersonaTags |
| Agent instructions | @wordbricks/persona/agent |
buildPersonaInstructions, PersonaLanguage |
| Schema | @wordbricks/persona/schema |
Drizzle tables, insert/select types, enums, PERSONA_EMBEDDING_DIMENSION |
The root export re-exports ./memory, ./schema, and ./agent, so most applications can import from @wordbricks/persona until they want a narrower module boundary.
Read RESPONSIBLE_USE.md before enabling personas for real users. Persona simulation of real people requires consent, authorization, or a carefully reviewed public-material-only basis. PERSONA_PROFILE_TYPES distinguishes fictional/composite characters, living public figures, deceased public figures, private authorized people, and synthetic roles; PERSONA_CONSENT_STATUSES records the consent basis. Real-person personas should not be activated with unknown consent outside controlled review, and disclosures should make clear that users are interacting with an AI persona system, not the biological person.
See RELEASING.md for the npm trusted-publishing setup and release procedure.
bun install
bun run test
bun run typecheck
bun run build