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

@itaylor/agentic-loop

v0.4.0

Published

A lightweight library for running agentic loops with LLMs and tool calling

Readme

agentic-loop

A lightweight, functional library for running agentic loops with LLMs, tool calling, and summarization support.

Installation

npm install @itaylor/agentic-loop

You'll also need an AI SDK provider package for whichever LLM you're using, e.g.:

npm install @ai-sdk/openai
# or @ai-sdk/anthropic, @ai-sdk/google, ai-sdk-ollama, etc.

Quick Start

import { runAgentSession } from "@itaylor/agentic-loop";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

// Simple case - await directly
const result = await runAgentSession(
  { languageModel: openai("gpt-4o") },
  {
    tools: {
      search: {
        description: "Search for information",
        inputSchema: z.object({ query: z.string() }),
        execute: async ({ query }) => ({ results: ["result1", "result2"] }),
      },
    },
  }
);

console.log(result.finalOutput);

// Advanced case - get sessionId immediately
const session = runAgentSession(modelConfig, sessionConfig);
console.log("Session started:", session.sessionId);
await logSessionStart(session.sessionId);
const result = await session.promise;

Features

  • Multi-turn conversations with automatic tool calling
  • Any AI SDK provider (OpenAI, Anthropic, Google, Ollama, Mistral, and more, anything that is supported by AI SDK)
  • Built-in task completion - agents call task_complete when done
  • Session suspension - pause sessions to wait for external events (approval, async ops, etc.)
  • Session resumption - continue from saved messages after crashes
  • Token management - automatic summarization when approaching limits
  • Error handling - automatic retries with errors added to conversation
  • Idle detection - nudges agents stuck in thinking loops
  • Event callbacks - hooks for logging, persistence, monitoring
  • Functional design - no classes, pure functions

API

runAgentSession(modelConfig, sessionConfig): AgentSession

Returns immediately with session object containing sessionId, initialMessage, and promise. The session object is "thenable" - you can await it directly.

ModelConfig:

{
  languageModel: LanguageModel;                   // Any AI SDK LanguageModelV3 instance
  languageModelSettings?: LanguageModelSettings;  // Applied to every agent LLM call
  summaryLanguageModel?: LanguageModel;           // Optional separate model for summarization
  summaryLanguageModelSettings?: LanguageModelSettings; // Settings for summary model only
}

languageModel accepts any AI SDK-compatible model instance:

import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
import { google } from "@ai-sdk/google";
import { ollama } from "ai-sdk-ollama";

{ languageModel: openai("gpt-4o") }
{ languageModel: anthropic("claude-opus-4-5") }
{ languageModel: google("gemini-2.0-flash") }
{ languageModel: ollama("llama3.2") }

LanguageModelSettings covers temperature, topP, topK, maxOutputTokens, providerOptions, and other per-call settings:

// OpenAI reasoning effort
{ languageModelSettings: { providerOptions: { openai: { reasoningEffort: "high" } } } }

// Anthropic extended thinking
{ languageModelSettings: { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 10000 } } } } }

// Cheap fast model for summarization, expensive reasoning model for agent work
{
  languageModel: anthropic("claude-opus-4-5"),
  languageModelSettings: { providerOptions: { anthropic: { thinking: { type: "enabled", budgetTokens: 10000 } } } },
  summaryLanguageModel: anthropic("claude-haiku-4-5"),
  summaryLanguageModelSettings: { temperature: 0 },
}

When summaryLanguageModel is not provided, summarization uses languageModel + languageModelSettings. When summaryLanguageModel is provided, summaryLanguageModelSettings is fully independent — no fallback from the main settings.

AgentSessionConfig:

{
  systemPrompt?: string;        // Optional — defaults to a generic helpful assistant prompt
  tools: Record<string, Tool>;
  sessionId?: string;           // Auto-generated if not provided
  messages?: Message[];         // Resume from saved messages (ignored if empty)
  initialMessage?: string;      // Starting message for fresh sessions (ignored if messages provided)
  maxTurns?: number;            // Default: 50
  tokenLimit?: number;          // Trigger summarization
  llmTimeout?: number;          // LLM call timeout (ms)
  toolTimeout?: number;         // Tool call timeout (ms)
  logger?: Logger;              // Custom logger
  callbacks?: SessionCallbacks; // Event hooks
  metadata?: Record<string, any>;
}

AgentSession:

{
  sessionId: string;                // Available immediately
  initialMessage: string;           // The message that started the session
  promise: Promise<AgentSessionResult>;
  then: (...) => ...;               // Makes it awaitable
}

AgentSessionResult:

{
  sessionId: string;
  finalOutput: string;
  totalTurns: number;
  completionReason: "task_complete" | "max_turns" | "error" | "suspended";
  messages: Message[];
  taskResult?: any;    // Data from task_complete tool
  suspendInfo?: SessionSuspendInfo;  // Present if suspended
  error?: Error;
}

Callbacks

All callbacks receive sessionId as first parameter:

{
  onTurnStart?: (sessionId: string, turn: number) => void | Promise<void>;
  onAssistantMessage?: (sessionId: string, text: string, turn: number) => void | Promise<void>;
  onToolCall?: (sessionId: string, info: ToolCallInfo) => void | Promise<void>;
  onToolResult?: (sessionId: string, info: ToolResultInfo) => void | Promise<void>;
  onError?: (sessionId: string, info: ErrorInfo) => void | Promise<void>;
  onComplete?: (sessionId: string, info: SessionCompleteInfo) => void | Promise<void>;
  onMessagesUpdate?: (sessionId: string, messages: Message[]) => void | Promise<void>;
  
  // Summarization callbacks (library handles the LLM call)
  onBeforeSummarize?: (sessionId: string, messages: Message[]) => Message[] | Promise<Message[]>;
  onAfterSummarize?: (sessionId: string, summarizedMessages: Message[]) => Message[] | Promise<Message[]>;
  
  // Suspension callback
  onSuspend?: (sessionId: string, info: SessionSuspendInfo) => void | Promise<void>;
}

Messages

The Message type reflects the actual AI SDK message structure. Messages can contain simple text or complex content with tool calls, results, images, and files:

type Message = 
  | {
      role: "user";
      content: string | Array<TextPart | ImagePart | FilePart>;
    }
  | {
      role: "assistant";
      content: string | Array<TextPart | FilePart | ToolCallPart | ToolResultPart>;
    };

Simple text messages:

{ role: "user", content: "Hello!" }
{ role: "assistant", content: "Hi there!" }

Complex messages with tool calls/results: When the agent calls tools, the AI SDK automatically creates messages with structured content arrays containing ToolCallPart and ToolResultPart objects. These are handled internally by the library.

For most use cases, you can treat message content as strings. The library handles the complex formats automatically when tools are used.

Examples

Basic Usage

import { ollama } from "ai-sdk-ollama";

const result = await runAgentSession(
  { languageModel: ollama("qwen2.5:7b") },
  {
    tools: { /* your tools */ },
    initialMessage: "Summarize the three laws of thermodynamics.",
  }
);

With Persistence

const session = runAgentSession(modelConfig, {
  sessionId: generateId(),
  systemPrompt: "You are a specialized data analyst.",
  tools: myTools,
  callbacks: {
    onMessagesUpdate: async (sessionId, messages) => {
      await db.saveMessages(sessionId, messages);
    },
    onComplete: async (sessionId, info) => {
      await db.markComplete(sessionId, info);
    },
  },
});

console.log("Tracking session:", session.sessionId);
const result = await session.promise;

Resume After Crash

// Server crashed, restarting...
const savedMessages = await db.loadMessages("session-123");

const session = runAgentSession(modelConfig, {
  sessionId: "session-123",   // Same ID
  messages: savedMessages,    // Continue from here
  systemPrompt: "...",
  tools: myTools,
});

const result = await session.promise;

With MCP Tools

import { createMCPClient } from "@ai-sdk/mcp";

const mcpClient = await createMCPClient({ /* config */ });
const mcpTools = await mcpClient.tools();

const result = await runAgentSession(modelConfig, {
  tools: mcpTools,  // Pass MCP tools directly
  // ...
});

await mcpClient.close();

If you want to give your agent code editing capabilities (read/write files, search, apply patches, etc.), consider using agent-mcp — a fast stdio MCP server providing file system navigation, text search, code analysis, and patch application.

import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
import { createMCPClient } from "@ai-sdk/mcp";

const transport = new Experimental_StdioMCPTransport({
  command: "/path/to/agent-mcp",
  args: ["/path/to/your/repo"],
});

const mcpClient = await createMCPClient({ transport });
const mcpTools = await mcpClient.tools();

const result = await runAgentSession(modelConfig, {
  tools: mcpTools,
  initialMessage: "Review the codebase and fix any TypeScript errors you find.",
});

await mcpClient.close();

Get SessionId Immediately

const session = runAgentSession(modelConfig, sessionConfig);

// SessionId available right away
await monitoring.startTracking(session.sessionId);
await logSessionStart(session.sessionId, session.initialMessage);

// Then wait for completion
const result = await session.promise;

Token Summarization

The library automatically summarizes messages when approaching the token limit:

const result = await runAgentSession(modelConfig, {
  systemPrompt: "...",
  tools: myTools,
  tokenLimit: 100000, // ~25k tokens - triggers summarization
  callbacks: {
    // Optional: modify messages before summarization
    onBeforeSummarize: async (sessionId, messages) => {
      console.log(`[${sessionId}] Summarizing ${messages.length} messages`);
      // Example: keep last 10 messages out of summarization
      return messages.slice(0, -10);
    },
    
    // Optional: modify messages after summarization
    onAfterSummarize: async (sessionId, summarizedMessages) => {
      console.log(`[${sessionId}] Summary complete`);
      // Example: append the last 10 messages we kept
      return [...summarizedMessages, ...recentMessages];
    },
  },
});

How it works:

  1. Library detects token limit approaching
  2. Calls onBeforeSummarize(sessionId, messages) → returns messages to summarize
    • Default: all messages are summarized
    • Example: Remove system prompts, keep recent messages
  3. Library calls the LLM to summarize the messages
  4. Creates summary as 2 messages: "Previous conversation summary:" + summary text
  5. Calls onAfterSummarize(sessionId, summarizedMessages) → returns final messages
    • Default: just the summary (2 messages)
    • Example: Add back system prompts, append recent messages
  6. Replaces message history and continues

Examples:

// Keep last 10 messages
onBeforeSummarize: (sessionId, messages) => messages.slice(0, -10),
onAfterSummarize: (sessionId, summary) => [...summary, ...messages.slice(-10)],

// Remove system prompts before, add back after
onBeforeSummarize: (sessionId, messages) => 
  messages.filter(m => !m.content.startsWith('You are')),
onAfterSummarize: (sessionId, summary) => 
  [{ role: 'user', content: systemPrompt }, ...summary],

No LLM setup needed - library handles the heavy lifting!

Built-in Tools

task_complete

Every session includes this tool automatically:

// Agent calls this internally:
task_complete({
  summary: "Completed the analysis",
  result: { findings: [...] }
})

Signals graceful completion with completionReason: "task_complete".

Session Suspension

Agents can suspend their session to wait for external events (human approval, async operations, etc.). The session stops cleanly and can be resumed later, even after server restarts.

Creating a Suspendable Tool

Any tool can suspend a session by returning a special __suspend__ signal:

const result = await runAgentSession(modelConfig, {
  systemPrompt: "You are a helpful assistant that needs approval.",
  initialMessage: "Please request approval to proceed.",
  tools: {
    request_approval: {
      description: "Request approval from a human. Your session will pause until they respond.",
      inputSchema: z.object({
        action: z.string().describe("The action that needs approval"),
        reason: z.string().describe("Why this action is needed"),
      }),
      execute: async (args) => {
        // Store the approval request somewhere
        await db.createApprovalRequest(args);
        
        // Return suspension signal
        return {
          __suspend__: true,
          reason: "waiting_for_approval",
          data: {
            action: args.action,
            requestId: generateId(),
          },
        };
      },
    },
  },
  callbacks: {
    onSuspend: async (sessionId, info) => {
      console.log(`Session ${sessionId} suspended: ${info.reason}`);
      // Save suspension state to database
      await db.saveSuspendedSession(sessionId, info);
    },
  },
});

// Session stopped with completionReason: "suspended"
console.log(result.completionReason); // "suspended"
console.log(result.suspendInfo); // { reason: "waiting_for_approval", data: {...}, turn: 1 }

Resuming a Suspended Session

To resume, simply call runAgentSession again with the saved messages plus the response:

// Later, when approval arrives...
const suspendedSession = await db.loadSuspendedSession(sessionId);

const result = await runAgentSession(modelConfig, {
  sessionId: sessionId,  // Same session ID
  systemPrompt: "You are a helpful assistant that needs approval.",
  messages: [
    ...suspendedSession.messages,  // All previous messages
    {
      role: "user",
      content: "Approval granted! You may proceed. Call task_complete when done.",
    },
  ],
  tools: {}, // Same tools or empty if no longer needed
});

// Agent continues from where it left off
console.log(resumedResult.completionReason); // "task_complete"

Persistence Across Restarts

The suspension state is just data - it survives server restarts:

// Before restart - save everything
const result = await runAgentSession(modelConfig, config);
if (result.completionReason === "suspended") {
  await fs.writeFile(`sessions/${result.sessionId}.json`, JSON.stringify({
    sessionId: result.sessionId,
    messages: result.messages,
    suspendInfo: result.suspendInfo,
  }));
}

// --- SERVER RESTART ---

// After restart - load and resume
const saved = JSON.parse(await fs.readFile(`sessions/${sessionId}.json`));
const resumedResult = await runAgentSession(modelConfig, {
  sessionId: saved.sessionId,
  messages: [
    ...saved.messages,
    { role: "user", content: "External data has arrived: {...}" },
  ],
  // ... rest of config
});

Use Cases

  • Human approval workflows - Agent requests permission, waits for response
  • Async API calls - Wait for webhook callbacks or long-running operations
  • Multi-agent coordination - Agent asks another agent a question, blocks until answered
  • Rate limiting - Suspend when rate limited, resume when quota refreshes
  • Scheduled operations - Suspend until a specific time

Multiple Suspensions

Sessions can suspend and resume multiple times:

let result = await runAgentSession(modelConfig, config);

// First suspension
assert.equal(result.completionReason, "suspended");
result = await runAgentSession(modelConfig, {
  messages: [...result.messages, { role: "user", content: "Step 1 done" }],
  // ...
});

// Second suspension
assert.equal(result.completionReason, "suspended");
result = await runAgentSession(modelConfig, {
  messages: [...result.messages, { role: "user", content: "Step 2 done" }],
  // ...
});

// Final completion
assert.equal(result.completionReason, "task_complete");

See examples/suspension.ts for complete working examples.

Error Handling

  • LLM errors - Retry with error in conversation
  • Tool parsing errors - Report to agent for correction
  • Tool execution errors - Caught and logged
  • Timeouts - Configurable for LLM and tools
const result = await runAgentSession(modelConfig, {
  llmTimeout: 60000,  // 60 seconds
  toolTimeout: 30000, // 30 seconds
  callbacks: {
    onError: async (sessionId, info) => {
      console.error(`[${sessionId}] Error in ${info.phase}:`, info.error);
      await monitoring.logError(sessionId, info);
    },
  },
});

Idle Detection

If agent produces text without calling tools for 2 turns, a reminder is automatically sent:

REMINDER: If you have completed your task, you must call the task_complete 
tool with a summary. If you are not done yet, please continue working.

Architecture

Functional design - Pure functions, no classes Event-driven - Callbacks for all state changes Provider-agnostic - Works with any AI SDK provider Minimal dependencies - No MCP, no file I/O, no frameworks Separation of concerns - Library handles loop, caller handles tools/persistence

License

MIT