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

saico

v2.9.2

Published

Hierarchical AI Conversation Orchestrator - Task hierarchy with conversation contexts

Readme

Saico - Simple AI-Agent Conversation Orchestrator

Saico is a Node.js library for building AI agents with hierarchical conversations, automatic context aggregation, and enterprise-grade tool calling. It manages nested task trees where each node can have its own message queue, system prompt, tools, and state — and the library automatically assembles the full payload sent to the LLM by walking the tree.

Features

  • Hierarchical conversations — Parent-child task trees with automatic prompt, tool, and state summary aggregation
  • Token-aware summarization — Automatic summarization when message history approaches token limits
  • Tool calling — Depth control, deferred execution, duplicate detection, repetition prevention, and timeout handling
  • Pluggable storage — Optional Redis persistence (auto-save via proxy), library-level backend registration (Saico.registerBackend), and pluggable DB backends (DynamoDB adapter included)
  • Isolation boundariesopt.isolate stops ancestor aggregation at any node in the tree
  • Serialization — Full state save/restore for long-running agents

Installation

npm install saico

Quick Start

const { Saico } = require('saico');

class MyAgent extends Saico {
    constructor() {
        super({
            name: 'my-agent',
            prompt: 'You are a helpful assistant.',
            functions: [{
                type: 'function',
                function: {
                    name: 'get_weather',
                    description: 'Get weather for a location',
                    parameters: {
                        type: 'object',
                        properties: { location: { type: 'string' } },
                        required: ['location']
                    }
                }
            }]
        });
    }

    // Tool implementations — define TOOL_ prefix methods
    async TOOL_get_weather(args) {
        return `Weather in ${args.location}: 72F, sunny`;
    }
}

const agent = new MyAgent();
agent.activate({ createQ: true });

// Backend message (prefixed with [BACKEND] automatically)
const reply = await agent.sendMessage('What is the weather in Tokyo?');

// User-facing chat message (routed to deepest active msgs Q)
const chatReply = await agent.recvChatMessage('Hello!');

Core Concepts

Saico Lifecycle

Saico separates construction from activation:

// 1. Construct — sets up config, Redis proxy, DB access. No task yet.
const agent = new Saico({
    name: 'agent',
    prompt: 'System prompt here',
    createQ: true,  // message Q created on activate()
    dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
});

// DB methods work before activation
const item = await agent.dbGetItem('id', '123');

// 2. Activate — creates internal task + message Q (from this.createQ)
agent.activate();

// 3. Use — send messages, spawn children
await agent.sendMessage('Do something');
await agent.recvChatMessage('User says hello');

// 4. Deactivate — bubbles cleaned messages to parent, closes msgs Q
await agent.deactivate();

Subclasses can also define this.states (task functions) in the constructor — activate() picks them up automatically:

class MyAgent extends Saico {
    constructor() {
        super({ name: 'agent', prompt: 'You are helpful', createQ: true });
        this.states = [
            async function main() {
                return await this.sendMessage('Starting...');
            }
        ];
    }
}
const agent = new MyAgent();
agent.activate();  // no params needed — uses this.createQ and this.states

Message Orchestration

When sendMessage() or recvChatMessage() is called, Saico walks the parent chain to build the full LLM payload:

Root Saico (prompt: "You are a manager")
  +-- Child Saico (prompt: "Handle bookings")
       +-- Grandchild Saico (prompt: "Process payment")
            sendMessage("Charge $50")
                 |
                 v
    Preamble built automatically:
    [Root prompt] [Root state summary] [Root tool digest]
    [Child prompt] [Child state summary + recent msgs] [Child tool digest]
    [Grandchild prompt] [Grandchild state summary]
    ... then the actual message queue messages ...

    Functions aggregated from all levels.
  • sendMessage(content, functions, opts) — Sends a backend message (auto-prefixed [BACKEND]). Uses the current or nearest ancestor msgs Q.
  • recvChatMessage(content, opts) — Routes a user chat message DOWN to the deepest descendant with a message queue.

Isolation

Set isolate: true to prevent ancestor aggregation:

const isolated = new Saico({
    name: 'isolated-agent',
    prompt: 'Independent context',
    isolate: true  // won't include parent prompts/tools/summaries
});

State Summaries

Override getStateSummary() in your subclass to provide dynamic state context:

class OrderAgent extends Saico {
    getStateSummary() {
        return `Active order: #${this.orderId}, items: ${this.items.length}`;
    }
}

When a Saico's msgs Q is not the deepest active one, its last 5 user/assistant messages are also included in the state summary automatically.

Spawning Child Saico Instances

// Child with its own msgs Q (auto-activated by spawn)
const child = new Saico({
    name: 'subtask',
    prompt: 'Handle this specific sub-task',
    createQ: true,
    functions: [/* child-specific tools */],
});
agent.spawn(child);
await child.sendMessage('Working on subtask...');

// Child without msgs Q (uses parent's via findMsgs())
const simple = new Saico({ name: 'simple' });
agent.spawn(simple);
await simple.sendMessage('Quick operation');

// spawnAndRun: spawn + schedule child task to run on nextTick
const runner = new Saico({ name: 'runner' });
runner.states = [async function() { return await this.sendMessage('Go'); }];
agent.spawnAndRun(runner);

Parent must be activated before calling spawn() or spawnAndRun(). Children are auto-activated if needed.

Deactivation and Message Bubbling

When a Saico deactivates, cleaned messages (no tool calls, no [BACKEND] messages) are pushed into the parent's message queue, preserving conversation continuity.

Constructor Options

new Saico({
    // Identity
    id: 'custom-id',           // Auto-generated if omitted
    name: 'my-agent',          // Defaults to class name

    // AI config
    prompt: 'System prompt',
    functions: [],             // OpenAI function definitions
    createQ: false,            // Create message Q on activate() (also settable as this.createQ)

    // Behavior
    isolate: false,            // Stop ancestor aggregation

    // Session config (defaults for this agent and its children)
    token_limit: 4000,
    max_depth: 5,              // Max tool call recursion depth
    max_tool_repetition: 20,   // Max consecutive repeated tool calls
    queue_limit: 100,          // Message queue limit
    min_chat_messages: 5,      // Min messages to keep in queue
    sessionConfig: {},         // Override any of the above

    // Storage
    redis: true,               // Set false to skip Redis proxy
    key: 'custom-redis-key',
    store: 'my-table',         // Table name for instance persistence (store/closeSession/restore)
    dynamodb: {                // DynamoDB config (creates instance-level adapter)
        region: 'us-east-1',
        credentials: { accessKeyId: '...', secretAccessKey: '...' },
    },
    db: customAdapter,         // Any adapter with put/get/delete/query interface

    // User data
    userData: {},              // Arbitrary user metadata
});

Activate Options

agent.activate({
    createQ: true,             // Override this.createQ for this activation
    prompt: 'Extra prompt',    // Appended to class-level prompt
    states: [],                // Override this.states for this activation
    taskId: 'custom-id',
    sequential_mode: true,     // Process messages sequentially

    // Override session config for this activation
    token_limit: 8000,
    max_depth: 10,
    queue_limit: 200,
});

User Data

agent.setUserData('preference', 'dark-mode');  // returns this (chainable)
agent.getUserData('preference');                // 'dark-mode'
agent.getUserData();                            // { preference: 'dark-mode' }
agent.clearUserData();                          // returns this

Session Info

agent.getSessionInfo();
// {
//   id, name, running, completed,
//   messageCount, childCount,
//   userData, uptime
// }

await agent.store();         // save to registered backend independently
await agent.closeSession();  // store + cancel task

// Restore from registered backend
const restored = await Saico.restore(agent.id, { store: 'sessions' });

Database Access

Saico provides backend-agnostic DB methods. Configure via Saico.registerBackend('dynamodb', config) (library-level), opt.dynamodb (instance-level auto-creates adapter), or opt.db (any adapter). Key, key value, and table default to 'id', this.id, and this._storeName — so operating on own record is a one-liner. Child Saico instances inherit the parent's adapter via _getDb(), which also falls back to the registered backend.

// CRUD — shorthand (defaults: key='id', value=this.id, table=this._storeName)
const me = await agent.dbGetItem();                   // get own record
await agent.dbDeleteItem();                           // delete own record
// Explicit
await agent.dbPutItem({ id: '123', name: 'test' }, 'my-table');
const item = await agent.dbGetItem('id', '123', 'my-table');
await agent.dbDeleteItem('id', '123', 'my-table');
const items = await agent.dbQuery('email-index', 'email', '[email protected]', 'my-table');
const all = await agent.dbGetAll('my-table');

// Updates — setKey, item first; key, keyValue, table last (with defaults)
await agent.dbUpdate('status', 'active');             // update own record
await agent.dbUpdate('status', 'active', 'id', '123', 'my-table');
await agent.dbUpdatePath([{ key: 'nested' }], 'field', 'value');
await agent.dbListAppend('tags', 'new-tag');

// Counters
const nextId = await agent.dbNextCounterId('OrderId', 'counters');
const count = await agent.dbGetCounterValue('OrderId', 'counters');
await agent.dbSetCounterValue('OrderId', 100, 'counters');
const total = await agent.dbCountItems('my-table');

Override _deserializeRecord(raw) to transform raw DB records on retrieval (e.g., restore class instances):

class MyAgent extends Saico {
    _deserializeRecord(raw) {
        if (raw.type === 'order') return new Order(raw);
        return raw;
    }
}

Serialization

Both serialize() and Saico.deserialize() are async. serialize() calls prepareForStorage() first (strips _ props, skips functions/states, compresses msgs) then JSON.stringifys the result.

// prepareForStorage — clean snapshot
const data = await agent.prepareForStorage();

// serialize/deserialize
const json = await agent.serialize();
const restored = await Saico.deserialize(json);

// Durable persistence (uses registered backend + opt.store table name)
await agent.store();         // save independently
await agent.closeSession();  // store + cancel task
const restored2 = await Saico.restore(agent.id, { store: 'sessions' });

prepareForStorage() automatically picks up all non-underscore properties (id, name, prompt, userData, sessionConfig, tm_create, isolate, etc.) and produces compressed chat_history for the msgs Q.

Initialization

const { Saico, init } = require('saico');

// Initialize Redis (default: enabled) and register DynamoDB backend
await init({
    dynamodb: { region: 'us-east-1', credentials: { accessKeyId: 'AK', secretAccessKey: 'SK' } },
});

// Or register backend directly
Saico.registerBackend('dynamodb', { region: 'us-east-1', credentials: { ... } });

Redis Persistence

When Redis is initialized (default: enabled via init()), Saico instances are automatically wrapped in an observable proxy. Any property change triggers a debounced save to Redis.

const agent = new Saico({ name: 'persistent-agent' });
agent.someProperty = 'value';  // Auto-saved to Redis

Properties prefixed with _ are internal and not persisted.

Tool Implementation (TOOL_ methods)

Define tool implementations as TOOL_-prefixed methods on your Saico subclass. When the LLM returns a tool call, Saico automatically searches the task hierarchy (current → up parents → down children) to find and invoke the matching method with parsed arguments.

class MyAgent extends Saico {
    async TOOL_get_weather(args) {
        // args is already JSON.parse'd
        return `Weather in ${args.location}: 72F, sunny`;
    }

    async TOOL_search(args) {
        const results = await search(args.query);
        return { content: JSON.stringify(results), functions: updatedTools };
    }
}

Return a string or { content: string, functions?: [] }.

Tool Safety Features

  • Depth controlmax_depth (default: 5) prevents infinite tool call recursion
  • Deferred execution — Tool calls defer when max depth is reached, resume when depth reduces
  • Duplicate detection — Identical active tool calls are blocked
  • Repetition preventionmax_tool_repetition (default: 20) blocks excessive repeated calls
  • Timeout handling — Configurable timeout (default: 5s) with graceful failure
  • Message queuing — Messages queue automatically when tool calls are pending

Low-Level API

For cases where you need a standalone message queue without the Saico master class:

const { createMsgs } = require('saico');

// Standalone message queue
const ctx = createMsgs('System prompt', { tag: 'my-tag', token_limit: 4000 });
const reply = await ctx.sendMessage('user', 'Hello', functions);

Project Structure

saico/
+-- index.js      # Thin barrel file, exports all components
+-- saico.js      # Saico master class — owns msgs Q, spawn, DB, orchestration
+-- itask.js      # Pure task runner — hierarchy, states, cancellation, promises
+-- msgs.js       # Conversation context (message queue, tool calls, summarization)
+-- dynamo.js     # DynamoDB storage adapter
+-- store.js      # Minimal storage shell (Redis helper + ID generation)
+-- openai.js     # OpenAI API wrapper with retry logic
+-- redis.js      # Redis persistence with observable proxy
+-- util.js       # Utilities (token counting, logging)

Testing

npm test

300 tests covering Saico lifecycle, msgs Q ownership, spawn/spawnAndRun, task hierarchy, message handling, tool calls, DB adapters, async serialization, prepareForStorage, backend registration, persistence (store/closeSession/restore via registered backend), storage integration, and full hierarchy flows.

Requirements

  • Node.js >= 16.0.0
  • OPENAI_API_KEY environment variable for LLM calls
  • Redis (optional, for auto-persistence)
  • AWS SDK v3 (optional peer dependency, for DynamoDB)

License

ISC