@forgrit/ai-personas
v1.0.0
Published
Lightweight AI persona framework: data-as-personas, provider-agnostic conversation orchestration, pre-flight guardrails, tool-declaration helpers. Bring your own LLM client.
Maintainers
Readme
@forgrit/ai-personas
A lightweight, provider-agnostic AI persona framework. Define personas as data, orchestrate conversations against any LLM, gate inputs with pre-flight guardrails. Bring your own LLM client.
Status
🚀 v1.0.0 — General Availability (released 2026-05-27)
Battle-tested across 2 production consumers (anonymous chat surface + AI-Ops orchestrator) before its first npm release. The public surface listed below under "Public surface" is SemVer-stable:
- Breaking changes: major bumps only (v2.0.0, v3.0.0, …)
- New additions: minor bumps (v1.1.0, v1.2.0, …)
- Bug fixes: patch bumps (v1.0.1, v1.0.2, …)
See CHANGELOG.md for full release history. See AUTHORING.md for a comprehensive persona-authoring guide.
What's in the box
Types (Persona, Tool, Guardrail, conversation primitives):
Persona,PersonaMessage,PersonaConversation,MessageRole,PreparedTurnTool<TInput>,ToolDeclarationGuardrail,GuardrailResultFewShotExampleModelPreferences
Runtime (orchestration + helpers):
ConversationOrchestrator— central orchestration classTurnBuilder— message-array composerContextWindowManager— token-budget trimmer (drops history, preserves few-shot anchor)GuardrailEnforcer— sequential pre-flight evaluatorMemoryPersonaRuntime— deterministic test stub (echo runtime)IPersonaRuntime— adapter interface (consumers implement)validatePersona— Zod-backed structural checktoolToDeclaration— Zod → JSON Schema bridge
Reference personas (data only — generic, not domain-specific):
pmAssistantPersona— PM coaching: clarifying questions + Given/When/Then draftingarchitectReviewerPersona— architecture review against widely-shared principles
Convention helpers:
PERSONA_IDS+PersonaId— reserved-ID const tuple
What it's NOT
- Not a NestJS module — direct CommonJS imports; future
@forgrit/ai-personas-nestjsmay ship. - Not coupled to any provider SDK — consumers implement
IPersonaRuntimeagainst their LLM client. - Not a tool-execution runtime — declarations only; the consumer
dispatches on
nameand runs the handler. - Not a streaming client — Node-only CJS; the orchestrator returns static turn payloads (consumers wrap streaming in their adapter).
Install
npm install @forgrit/ai-personas
# or
pnpm add @forgrit/ai-personasQuick example (no LLM call)
import {
ConversationOrchestrator,
ContextWindowManager,
GuardrailEnforcer,
MemoryPersonaRuntime,
pmAssistantPersona,
} from '@forgrit/ai-personas';
const orchestrator = new ConversationOrchestrator(
new MemoryPersonaRuntime(),
new ContextWindowManager(),
new GuardrailEnforcer(),
);
const conv = {
id: 'c1',
personaId: pmAssistantPersona.id,
history: [],
createdAt: new Date(),
updatedAt: new Date(),
};
const result = await orchestrator.executeTurn(
pmAssistantPersona,
conv,
'Help me write acceptance criteria for a CSV export feature.',
);
console.log(result);
// "[stub:claude-opus-4-5] echo: Help me write acceptance criteria…"Wire it up — Anthropic adapter
import Anthropic from '@anthropic-ai/sdk';
import type { IPersonaRuntime, PreparedTurn } from '@forgrit/ai-personas';
export class MyAnthropicRuntime implements IPersonaRuntime {
constructor(private readonly client: Anthropic) {}
async send(turn: PreparedTurn): Promise<string> {
const response = await this.client.messages.create({
model: turn.modelPreferences.model,
max_tokens: turn.modelPreferences.maxTokens,
temperature: turn.modelPreferences.temperature,
system: turn.systemPrompt,
messages: turn.messages.map((m) => ({
role: m.role === 'system' ? 'user' : m.role,
content: m.content,
})),
tools: turn.tools.map((t) => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
})),
});
const first = response.content[0];
return first?.type === 'text' ? first.text : '';
}
}Wire it up — OpenAI adapter
import OpenAI from 'openai';
import type { IPersonaRuntime, PreparedTurn } from '@forgrit/ai-personas';
export class MyOpenAIRuntime implements IPersonaRuntime {
constructor(private readonly client: OpenAI) {}
async send(turn: PreparedTurn): Promise<string> {
const response = await this.client.chat.completions.create({
model: turn.modelPreferences.model,
max_tokens: turn.modelPreferences.maxTokens,
temperature: turn.modelPreferences.temperature,
messages: [{ role: 'system', content: turn.systemPrompt }, ...turn.messages],
tools: turn.tools.map((t) => ({
type: 'function',
function: {
name: t.name,
description: t.description,
// OpenAI calls it 'parameters'; @forgrit/ai-personas calls it 'input_schema'.
parameters: t.input_schema,
},
})),
});
return response.choices[0]?.message.content ?? '';
}
}Authoring your own persona
import { z } from 'zod';
import { validatePersona } from '@forgrit/ai-personas';
import type { Persona } from '@forgrit/ai-personas';
export const codeReviewerPersona: Persona = {
id: 'code-reviewer',
name: 'Code Reviewer',
description: 'Reviews code diffs against project conventions.',
systemPrompt: 'You are a code reviewer. Be concise. Flag bugs first.',
modelPreferences: {
model: 'claude-opus-4-5',
temperature: 0.2,
maxTokens: 4096,
maxContextTokens: 200_000,
},
tools: [
{
name: 'flag-issue',
description: 'Flag one issue in the diff',
inputSchema: z.object({
severity: z.enum(['low', 'medium', 'high']),
message: z.string().min(1),
}),
},
],
guardrails: [],
fewShotExamples: [],
};
// Optional: validate at module load (recommended).
validatePersona(codeReviewerPersona);Guardrails
Guardrails are pre-flight assertions. They run BEFORE the LLM call. A guardrail MAY reject a turn but MAY NOT mutate it.
import type { Guardrail } from '@forgrit/ai-personas';
// Reject turns whose last user message looks like a credit-card-shaped
// pattern. Returns first allow:false; never mutates the turn.
const noCardNumbers: Guardrail = (turn) => {
const last = turn.messages[turn.messages.length - 1]?.content ?? '';
if (/\b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}\b/.test(last)) {
return { allow: false, reason: 'piiDetected' };
}
return { allow: true };
};The GuardrailResult discriminated union has exactly two states:
{ allow: true } or { allow: false, reason }. There is no
"modified" state by design — mutation would let a guardrail silently
re-shape a turn.
Engine requirements
Node.js 20 or later.
License
MIT. See LICENSE.
