npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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, PreparedTurn
  • Tool<TInput>, ToolDeclaration
  • Guardrail, GuardrailResult
  • FewShotExample
  • ModelPreferences

Runtime (orchestration + helpers):

  • ConversationOrchestrator — central orchestration class
  • TurnBuilder — message-array composer
  • ContextWindowManager — token-budget trimmer (drops history, preserves few-shot anchor)
  • GuardrailEnforcer — sequential pre-flight evaluator
  • MemoryPersonaRuntime — deterministic test stub (echo runtime)
  • IPersonaRuntime — adapter interface (consumers implement)
  • validatePersona — Zod-backed structural check
  • toolToDeclaration — Zod → JSON Schema bridge

Reference personas (data only — generic, not domain-specific):

  • pmAssistantPersona — PM coaching: clarifying questions + Given/When/Then drafting
  • architectReviewerPersona — 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-nestjs may ship.
  • Not coupled to any provider SDK — consumers implement IPersonaRuntime against their LLM client.
  • Not a tool-execution runtime — declarations only; the consumer dispatches on name and 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-personas

Quick 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.

Issues

github.com/forgrit-ai/forgrit/issues