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

agentic-ai-framework

v1.0.0

Published

Reusable agentic framework with session memory, tool calling, CoT, and multi-agent team support

Readme

@insider/agent-framework

Reusable agentic framework for Node.js (ES Modules). Provides session memory, LLM-driven tool-calling, Chain-of-Thought, and multi-agent team coordination with pluggable LLM providers.


Installation

// In another package inside the monorepo
import { createAgent, createTeam, Tool } from '@insider/agent-framework';

Quick Start

import 'dotenv/config';
import { createAgent, Tool } from '@insider/agent-framework';

const agent = createAgent({
    name: 'my-agent',
    provider: 'grok',                           // 'grok' | 'claude' | 'openai'
    apiKey: process.env.GROK_API_KEY,
    systemPromptTemplate: 'You are a helpful assistant for ${userName}.',
    chainOfThought: true,
});

agent.registerTool(new Tool({
    name: 'get_data',
    description: 'Fetch data by ID',
    parameters: {
        type: 'object',
        properties: { id: { type: 'string' } },
        required: ['id'],
    },
    handler: async ({ id }) => `Data for ${id}`,
}));

agent.setContext('userName', 'Alice');
const result = await agent.run('Fetch data for item-42');
console.log(result.text);

Core Concepts

Agent

The main class. Each agent owns an LLM provider, tool registry, session memory, and a prompt builder.

One instance per request — create a fresh agent per HTTP request. Instances have no shared mutable state between themselves.

import { createAgent } from '@insider/agent-framework';

const agent = createAgent(options);  // shorthand for: new Agent(new AgentConfig(options))

AgentConfig Options

All options passed to createAgent(). Unknown keys throw an error (strict validation).

| Option | Type | Default | Description | |--------|------|---------|-------------| | name | string | required | Unique agent name | | provider | 'grok'│'claude'│'openai' | required | LLM provider | | apiKey | string | required | Provider API key | | systemPromptTemplate | string | one of these required | Inline system prompt with ${var} placeholders | | systemPromptFile | string | one of these required | Path to .md/.txt prompt file | | model | string | provider default | Model ID | | temperature | number | 0.1 | Sampling temperature (0–2) | | maxTokens | number | 4000 | Max output tokens | | maxToolIterations | number | 5 | Max tool-calling loop iterations | | loopTimeoutMs | number | 300000 | Tool loop timeout in ms (5 min) | | requestTimeoutMs | number | provider default | Individual LLM request timeout | | maxHistoryMessages | number | 50 | Max session history messages | | persistenceDir | string | null | Directory for session JSON files | | chainOfThought | boolean | true | Inject CoT reasoning block into prompt | | cotMode | 'prompt'│'reflect' | 'prompt' | CoT strategy | | cotStyle | 'step-by-step'│'pros-cons'│'custom' | 'step-by-step' | CoT block style | | cotCustomInstructions | string | null | Custom CoT text (when cotStyle='custom') | | description | string | '' | Agent description (used by AgentTeam) | | outputSchema | object | null | JSON Schema for structured output |


Tools

Defining a Tool

import { Tool } from '@insider/agent-framework';

const myTool = new Tool({
    name: 'search_docs',                  // snake_case, unique
    description: 'Search documentation', // shown to the LLM — be descriptive
    parameters: {                         // JSON Schema object
        type: 'object',
        properties: {
            query:  { type: 'string',  description: 'Search query' },
            limit:  { type: 'number',  description: 'Max results' },
            strict: { type: 'boolean', description: 'Exact match only' },
        },
        required: ['query'],
    },
    handler: async ({ query, limit = 10, strict }) => {
        // Do real work here
        return `Results for "${query}"`;  // return string or JSON-serializable object
    },
});

Zod schema is also accepted for parameters:

import { z } from 'zod';

const myTool = new Tool({
    name: 'create_ticket',
    description: 'Create a support ticket',
    parameters: z.object({
        title:    z.string(),
        priority: z.enum(['low', 'medium', 'high']),
        assignee: z.string().optional(),
    }),
    handler: async ({ title, priority, assignee }) => {
        // ...
    },
});

Supported Zod types: z.string(), z.number(), z.boolean(), z.array(), z.enum(), z.object(), z.optional(), z.nullable(), z.default(). All others throw — use a plain JSON Schema object for complex types.

Registering Tools

agent.registerTool(myTool);               // single tool
agent.registerTools([tool1, tool2]);       // multiple tools — both return agent (chainable)

// Chainable
agent
    .registerTool(searchTool)
    .registerTool(createTool);

Handler Contract

  • Receives parsed argument object from the LLM
  • Returns string or any JSON-serializable value (objects are auto-stringified)
  • Throwing an error causes the runner to report the error to the LLM as the tool result — the LLM can then decide how to recover

Session Memory

Stateful (Session) Pattern

// Start or resume a named session
const sessionId = await agent.startSession('user-123');

// Inject variables into system prompt as ${key}
agent.setContext('userName', 'Alice');
agent.setContext('role', 'admin');

const r1 = await agent.run('What tickets are assigned to me?');
const r2 = await agent.run('Show only the high-priority ones.'); // remembers previous turn

await agent.saveSession();    // persist to disk (requires persistenceDir in config)
await agent.endSession();     // clear in-memory state
// await agent.endSession(true); // also delete the file on disk

Resuming a session restores history and context from disk:

// Next request — session restored automatically
await agent.startSession('user-123');  // loads history from {persistenceDir}/user-123.json

Stateless (Per-Request) Pattern

// No startSession() needed — pass appendToHistory: false
const result = await agent.run(userInput, { appendToHistory: false });

Context Values

setContext(key, value) injects values into system prompt ${key} placeholders. Values must be JSON-serializable (strings, numbers, booleans, arrays, plain objects). Functions and Symbols are rejected.

agent.setContext('companyName', 'Insider');
agent.setContext('userTier', 'enterprise');
// System prompt: "You are an assistant for ${companyName} users on the ${userTier} plan."
// Rendered as: "You are an assistant for Insider users on the enterprise plan."

AgentResult

Every agent.run() returns an AgentResult:

{
    success: true,               // false if LLM failed or max iterations reached
    text: 'Final answer...',     // convenience: best text representation of the answer
    content: 'Raw LLM output',  // raw text from the last LLM response
    parsed: null,                // parsed JSON when outputSchema is set
    toolCallHistory: [           // all tool calls that happened in this run
        {
            id: 'call_abc',
            name: 'get_data',
            arguments: { id: 'item-42' },
            result: 'Data for item-42',
            iteration: 1,
            timestamp: '2026-03-13T10:00:00.000Z',
        }
    ],
    usage: {
        promptTokens: 120,
        completionTokens: 45,
        totalTokens: 165,
    },
    iterations: 1,               // number of tool-calling iterations
    error: undefined,            // error message when success=false
    cotTrace: undefined,         // present when cotMode='reflect'
}

Chain of Thought (CoT)

Prompt Mode (default)

A reasoning block is appended to the system prompt. Zero extra LLM calls.

createAgent({
    // ...
    chainOfThought: true,
    cotMode: 'prompt',           // default
    cotStyle: 'step-by-step',    // default
});

Available styles:

  • 'step-by-step' — numbered reasoning steps before answering
  • 'pros-cons' — trade-off analysis before deciding
  • 'custom' — provide your own instructions via cotCustomInstructions

Reflect Mode

After the tool-calling loop produces a final answer, a second LLM call verifies it. Use for high-stakes agents where accuracy matters more than cost.

createAgent({
    // ...
    chainOfThought: true,
    cotMode: 'reflect',
});

// result.cotTrace = { original, reflected, changed: true/false }

Multi-Agent Teams

Router Mode

The coordinator LLM decides which specialist(s) to call via tool-calling. Maps to the existing MasterAgent pattern.

import { createAgent, createTeam } from '@insider/agent-framework';

const sqlAgent = createAgent({
    name: 'sql-agent',
    provider: 'grok',
    apiKey: process.env.GROK_API_KEY,
    description: 'Answers questions by generating SQL queries',
    systemPromptTemplate: 'You are a SQL specialist. Table: tickets(id, status, priority).',
});

const ragAgent = createAgent({
    name: 'rag-agent',
    provider: 'grok',
    apiKey: process.env.GROK_API_KEY,
    description: 'Searches documentation for policy questions',
    systemPromptTemplate: 'You are a knowledge base specialist.',
});

const coordinator = createAgent({
    name: 'coordinator',
    provider: 'grok',
    apiKey: process.env.GROK_API_KEY,
    systemPromptTemplate: `You orchestrate specialist agents.
Available specialists:
\${teamMembers}
Always delegate to the most appropriate specialist.`,
});

const team = createTeam({
    coordinator,
    members: [sqlAgent, ragAgent],
    mode: 'router',   // default
});

const result = await team.run('How many open tickets are there?');
console.log(result.final);           // final synthesized answer
console.log(result.memberResults);   // per-member results keyed by agent name

The ${teamMembers} placeholder in the coordinator's system prompt is auto-populated with the member list.

Parallel Mode

All members run simultaneously. The coordinator synthesizes all responses.

const team = createTeam({
    coordinator: synthesizerAgent,
    members: [sqlAgent, ragAgent],
    mode: 'parallel',
});

const result = await team.run('How many critical tickets are open and what is the SLA?');
// sqlAgent and ragAgent run in parallel, coordinator combines both answers

Use parallel mode when a question genuinely needs multiple specialists. Use router when only one specialist is needed per question.

TeamResult

{
    success: true,
    final: 'Synthesized answer...',
    memberResults: {
        'sql-agent': { success: true, text: '...', content: '...', toolCallHistory: [] },
        'rag-agent': { success: true, text: '...', content: '...', toolCallHistory: [] },
    },
    coordinatorResult: { /* AgentResult */ },
    mode: 'router',
    error: null,
}

// Helper methods
result.getMemberResult('sql-agent');   // get one member's result
result.getSuccessfulMembers();         // ['sql-agent', 'rag-agent']

Dynamic Member Management

team.addMember(analyticsAgent);        // add at runtime
team.removeMember('sql-agent');        // remove by name
team.getMembers();                     // ['rag-agent', 'analytics-agent']
team.getInfo();                        // coordinator + members info + mode

LLM Providers

Built-in Providers

| Name | API | Default Model | |------|-----|---------------| | 'grok' | xAI / OpenAI-compatible | grok-code-fast-1 | | 'claude' | Anthropic Messages API | claude-sonnet-4-20250514 | | 'openai' | OpenAI Chat Completions | gpt-4o |

// Grok
createAgent({ provider: 'grok', apiKey: process.env.GROK_API_KEY, model: 'grok-2', ... });

// Claude
createAgent({ provider: 'claude', apiKey: process.env.ANTHROPIC_API_KEY, model: 'claude-opus-4-20250514', ... });

// OpenAI
createAgent({ provider: 'openai', apiKey: process.env.OPENAI_API_KEY, model: 'gpt-4o-mini', ... });

Provider instances are cached by (provider, model, apiKey) — the same combination always returns the same instance.

Custom Provider

import { BaseLLMProvider, LLMRouter } from '@insider/agent-framework';

class MyProvider extends BaseLLMProvider {
    constructor(apiKey, model = 'my-model') {
        super();
        this.apiKey = apiKey;
        this.model = model;
    }

    async complete(prompt, options = {}) {
        const messages = options.messages ?? [{ role: 'user', content: prompt }];
        // call your API...
        return {
            content: 'response text',
            parsed: null,
            usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
            model: this.model,
            finishReason: 'stop',
            toolCalls: undefined,
            messages: [...messages, { role: 'assistant', content: 'response text' }],
        };
    }

    async completeWithSchema(prompt, schema, options = {}) { /* ... */ }
    async testConnection() { return true; }
}

LLMRouter.register('my-provider', MyProvider);

// Now usable in createAgent
createAgent({ provider: 'my-provider', apiKey: '...', ... });

Structured Output

Force the LLM to respond with a specific JSON shape:

const agent = createAgent({
    // ...
    outputSchema: {
        type: 'object',
        properties: {
            answer:     { type: 'string' },
            confidence: { type: 'number' },
            sources:    { type: 'array', items: { type: 'string' } },
        },
        required: ['answer', 'confidence'],
    },
});

const result = await agent.run('What is our refund policy?');
console.log(result.parsed.answer);       // string
console.log(result.parsed.confidence);  // number

Error Handling

Framework throws typed errors — catch them specifically:

import {
    AgentError, ToolError, LLMError, ConfigError, MemoryError
} from '@insider/agent-framework';

try {
    const result = await agent.run(userInput);
    if (!result.success) {
        console.error('Agent failed:', result.error);
    }
} catch (err) {
    if (err instanceof LLMError)    { /* API key bad, network down, etc. */ }
    if (err instanceof ToolError)   { /* tool registration or execution issue */ }
    if (err instanceof MemoryError) { /* session load/save failed */ }
    if (err instanceof ConfigError) { /* bad AgentConfig options */ }
    if (err instanceof AgentError)  { /* other agent-level issue */ }
}

agent.run() itself does not throw on LLM failures — it returns { success: false, error: '...' }. It only throws for programming errors (wrong arguments, missing session, etc.).

Transient LLM errors (rate limits, 5xx, timeouts) are automatically retried up to 2 times with exponential backoff before failing.


Logging

Structured JSON logging via pino. Set log level with the AGENT_LOG_LEVEL environment variable.

AGENT_LOG_LEVEL=debug node app.js   # debug | info | warn | error | silent

In development, if pino-pretty is installed, logs are pretty-printed. In production (NODE_ENV=production) plain JSON is used regardless.


Complete Backend API Handler Example

import { createAgent, Tool, AgentError } from '@insider/agent-framework';
import { queryDatabase } from './db.js';

// Agent definition is typically created once at module load
function buildSQLAgent() {
    const agent = createAgent({
        name: 'sql-agent',
        provider: 'grok',
        apiKey: process.env.GROK_API_KEY,
        systemPromptTemplate: `You are a SQL specialist for ${`\${companyName}`}.
Table: zendesk_tickets(id, status, assignee, created_at, priority, subject)`,
        chainOfThought: true,
        maxToolIterations: 3,
    });

    agent.registerTool(new Tool({
        name: 'run_sql',
        description: 'Execute a SQL query and return results',
        parameters: {
            type: 'object',
            properties: { sql: { type: 'string', description: 'SQL query to execute' } },
            required: ['sql'],
        },
        handler: async ({ sql }) => {
            const rows = await queryDatabase(sql);
            return JSON.stringify(rows);
        },
    }));

    return agent;
}

// Express / Fastify handler
export async function handleQuestion(req, res) {
    const { question, sessionId, companyName } = req.body;

    // Fresh agent per request — no shared state
    const agent = buildSQLAgent();
    agent.setContext('companyName', companyName);

    let activeSessionId;
    try {
        activeSessionId = await agent.startSession(sessionId);  // restores history if exists
        const result = await agent.run(question);

        if (!result.success) {
            return res.status(500).json({ error: result.error });
        }

        await agent.saveSession();

        return res.json({
            answer: result.text,
            sessionId: activeSessionId,
            toolsUsed: result.toolCallHistory.map(t => t.name),
        });

    } catch (err) {
        return res.status(500).json({ error: err.message });
    } finally {
        await agent.endSession();  // always clear in-memory state
    }
}

Public API Reference

createAgent(options)           → Agent
createTeam(options)            → AgentTeam

Agent
  .registerTool(tool)          → Agent (chainable)
  .registerTools(tools[])      → Agent (chainable)
  .getToolRegistry()           → ToolRegistry
  .startSession(sessionId?)    → Promise<string>
  .saveSession()               → Promise<void>
  .endSession(deletePersisted?) → Promise<void>
  .setContext(key, value)      → Agent (chainable)
  .getContext(key)             → any
  .run(input, opts?)           → Promise<AgentResult>
  .getInfo()                   → Object
  .testConnection()            → Promise<boolean>

AgentTeam
  .addMember(agent)            → void
  .removeMember(name)          → void
  .getMembers()                → string[]
  .run(input, opts?)           → Promise<TeamResult>
  .getInfo()                   → Object

Tool
  new Tool({ name, description, parameters, handler })
  .toDefinition()              → { name, description, parameters }
  .execute(args)               → Promise<string | object>

ToolRegistry
  .register(tool)              → void  (throws if duplicate)
  .registerOrReplace(tool)     → void
  .unregister(name)            → void
  .has(name)                   → boolean
  .listNames()                 → string[]
  .getDefinitions()            → Array
  .execute(name, args)         → Promise<string>

LLMRouter
  .get(provider, model, apiKey) → BaseLLMProvider
  .register(name, Class)        → void
  .clearCache()                 → void
  .listProviders()              → string[]

SessionMemory
  .appendExchange(user, assistant) → void
  .getHistory()                    → Array
  .setContext(key, value)          → void
  .getContext(key)                 → any
  .snapshot()                      → Object
  .restore(snapshot)               → void
  .clear()                         → void

MemoryManager
  .load(sessionId)             → Promise<Object | null>
  .save(sessionId, snapshot)   → Promise<void>
  .delete(sessionId)           → Promise<void>
  .list()                      → Promise<string[]>