# @toolpack-sdk/agents Build production-ready AI agents with channels, workflows, and event-driven architecture. [![npm version](https://img.shields.io/npm/v/@toolpack-sdk/agents.svg)](https://www.npmjs.com/package/@toolpack-sdk/agents) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) ## Features - **4 Built-in Agents** — Research, Coding, Data, Browser - **8 Channel Types** — Slack, Telegram, Discord, Email, SMS, Webhook, Scheduled, MCP - **Event-Driven** — Full lifecycle hooks and events - **Human-in-the-Loop** — `ask()` support for two-way channels - **Knowledge Integration** — Built-in RAG support with knowledge bases - **Agent Mind** — Persistent cognitive layer: goals, beliefs, reflections, cross-run recall - **Agent Spawning** — LLM-driven ephemeral sub-agents via `spawn_agent` / `spawn_agents_parallel` tools, with depth control and parallel execution - **Hot Reload** — File watcher + graceful restart so code changes take effect without dropping conversations - **Evals** — `EvalDataset`, `EvalRunner`, 4 scorer types, regression reports - **OTel Tracing** — OpenTelemetry interceptor for distributed traces - **Type-Safe** — Full TypeScript support ## Installation ```bash npm install @toolpack-sdk/agents ``` ## Stable API (Phase 4) The following APIs are stable and follow semantic versioning. Breaking changes will require a major version bump: - `BaseAgent` — Abstract base class for all agents - `BaseChannel` — Abstract base class for all channels - `AgentRegistry` — Registry for agents and channels - `AgentInput`, `AgentResult`, `AgentOutput` — Core data structures - `AgentTransport`, `LocalTransport`, `JsonRpcTransport` — Transport layer - `AgentJsonRpcServer` — JSON-RPC server for hosting agents - `AgentError` — Error class for agent failures ### Version Policy - **Major (X.y.z)** — Breaking API changes - **Minor (x.Y.z)** — New features, backward compatible - **Patch (x.y.Z)** — Bug fixes, backward compatible ## Quick Start ```typescript import { BaseAgent, AgentRegistry, SlackChannel } from '@toolpack-sdk/agents'; // 1. Create a channel const slack = new SlackChannel({ name: 'slack', token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, channel: '#support', }); // 2. Create an agent (channels live on the agent) class SupportAgent extends BaseAgent { name = 'support-agent'; description = 'Customer support agent'; mode = 'chat'; channels = [slack]; async invokeAgent(input) { const result = await this.run(input.message); return result; } } // 3. Single-agent: start directly const agent = new SupportAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); await agent.start(); // OR multi-agent: use AgentRegistry // const registry = new AgentRegistry([agent]); // await registry.start(); ``` ## Built-in Agents ### ResearchAgent Web research for summarization, fact-finding, and trend monitoring. ```typescript import { ResearchAgent } from '@toolpack-sdk/agents'; const agent = new ResearchAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); const result = await agent.invokeAgent({ message: 'Summarize recent AI developments', }); ``` **Mode:** `agent` | **Tools:** `web.search`, `web.fetch`, `web.scrape` ### CodingAgent Code generation, refactoring, debugging, and test writing. ```typescript import { CodingAgent } from '@toolpack-sdk/agents'; const agent = new CodingAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); const result = await agent.invokeAgent({ message: 'Refactor the auth module', }); ``` **Mode:** `coding` | **Tools:** `fs.*`, `coding.*`, `git.*`, `exec.*` ### DataAgent Database queries, reporting, data analysis, and CSV generation. ```typescript import { DataAgent } from '@toolpack-sdk/agents'; const agent = new DataAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); const result = await agent.invokeAgent({ message: 'Generate weekly signups report', }); ``` **Mode:** `agent` | **Tools:** `db.*`, `fs.*`, `http.*` ### BrowserAgent Web browsing, form interaction, and content extraction. ```typescript import { BrowserAgent } from '@toolpack-sdk/agents'; const agent = new BrowserAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); const result = await agent.invokeAgent({ message: 'Extract prices from acme.com/products', }); ``` **Mode:** `chat` | **Tools:** `web.fetch`, `web.screenshot`, `web.extract_links` ## Channels Channels connect agents to external services. They can be **two-way** (receive messages, support `ask()`) or **trigger-only** (send only, no `ask()` support). ### SlackChannel (Two-way) ```typescript const slack = new SlackChannel({ name: 'slack-support', token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, channel: '#support', port: 3000, }); ``` ### TelegramChannel (Two-way) ```typescript const telegram = new TelegramChannel({ name: 'telegram-bot', token: process.env.TELEGRAM_BOT_TOKEN, }); ``` ### WebhookChannel (Two-way) ```typescript const webhook = new WebhookChannel({ name: 'github-webhook', path: '/webhook/github', port: 3000, }); ``` ### ScheduledChannel (Trigger-only) Runs agents on cron schedules. Three modes: static cron, dynamic store (agent-driven), or hybrid. ```typescript // Static — fixed cron schedule const scheduler = new ScheduledChannel({ name: 'daily-report', cron: '0 9 * * 1-5', // 9am weekdays message: 'Generate daily report', }); // Dynamic — agent schedules its own jobs via scheduler.* tools import { SchedulerStore, createSchedulerTools } from '@toolpack-sdk/agents'; const store = new SchedulerStore({ dbPath: './scheduler.db' }); const dynamic = new ScheduledChannel({ name: 'dynamic', store }); // For Slack delivery, attach a named SlackChannel to the same agent and // call `this.sendTo('', output)` from within `invokeAgent()`. ``` ### DiscordChannel (Two-way) ```typescript const discord = new DiscordChannel({ name: 'discord-bot', token: process.env.DISCORD_BOT_TOKEN, guildId: 'your-guild-id', channelId: 'your-channel-id', }); ``` ### EmailChannel (Outbound-only) ```typescript const email = new EmailChannel({ name: 'email-alerts', from: 'bot@acme.com', to: 'team@acme.com', smtp: { host: 'smtp.gmail.com', port: 587, auth: { user: 'bot@acme.com', pass: process.env.SMTP_PASSWORD }, }, }); ``` ### SMSChannel (Configurable) Two-way when `webhookPath` is set, outbound-only otherwise. ```typescript // Two-way const sms = new SMSChannel({ name: 'sms-alerts', accountSid: process.env.TWILIO_ACCOUNT_SID, authToken: process.env.TWILIO_AUTH_TOKEN, from: '+1234567890', webhookPath: '/sms/webhook', port: 3000, }); // Outbound-only const smsOutbound = new SMSChannel({ name: 'sms-notifications', accountSid: process.env.TWILIO_ACCOUNT_SID, authToken: process.env.TWILIO_AUTH_TOKEN, from: '+1234567890', to: '+0987654321', }); ``` ### McpChannel (Two-way) Exposes a Toolpack agent as a tool in an MCP server. The agent appears in `tools/list` as `agent.` and is callable by any MCP client. ```typescript import { McpChannel } from '@toolpack-sdk/agents'; import { Toolpack } from 'toolpack-sdk'; const ch = new McpChannel({ name: 'mcp' }); const agent = new PrReviewerAgent({ channels: [ch] }); await agent.start(); const sdk = await Toolpack.init({ provider: 'anthropic', tools: true }); await sdk.startMcpServer({ transport: 'stdio', // or 'http' with port agents: [ch.asAgentDefinition(agent)], }); ``` `ch.asAgentDefinition(agent)` produces the entry that `startMcpServer` registers in `tools/list`. Each MCP `tools/call` for `agent.` is routed through the channel to `agent.invokeAgent()` and the output is returned as the tool result. ## Creating Custom Agents Extend `BaseAgent` to create custom agents: ```typescript import { BaseAgent } from '@toolpack-sdk/agents'; class MyAgent extends BaseAgent { name = 'my-agent'; description = 'My custom agent'; mode = 'agent'; async invokeAgent(input) { // Process the message const result = await this.run(input.message); // Send to a channel await this.sendTo('slack', result.output); return result; } } ``` ## Human-in-the-Loop Use `ask()` to pause execution and request human input (two-way channels only). `ask()` sends the question and returns immediately — the user's answer arrives on the **next** invocation, where you check `getPendingAsk()`. ```typescript class ApprovalAgent extends BaseAgent { name = 'approval-agent'; mode = 'agent'; async invokeAgent(input) { // Turn 2: check if we are waiting for an answer const pending = this.getPendingAsk(input.conversationId); if (pending && input.message) { return this.handlePendingAsk( pending, input.message, async (answer) => { if (answer.toLowerCase() === 'yes') { await this.sendTo('slack', 'Draft approved!'); return { output: 'Draft approved and sent.' }; } return { output: 'Draft discarded.' }; }, ); } // Turn 1: do some work, then ask for approval const draft = await this.run(`Draft a response to: ${input.message}`); return this.ask(`Here is my draft:\n\n${draft.output}\n\nApprove? (yes/no)`); } } ``` **Note:** `ask()` throws if called from trigger-only channels (ScheduledChannel, EmailChannel). It requires a registry — use `AgentRegistry`, not standalone `agent.start()`. ## Conversation History Store conversation history separately from domain knowledge: ```typescript import { InMemoryConversationStore } from '@toolpack-sdk/agents'; class SupportAgent extends BaseAgent { // In-memory store (development/single-process) conversationHistory = new InMemoryConversationStore(); async invokeAgent(input) { // History is automatically loaded before AI call // and stored after response const result = await this.run(input.message); return result; } } ``` **Features:** - Auto-assembles conversation history before each AI call (up to 3 000-token budget by default) - Auto-stores user and assistant messages via the capture interceptor - Auto-trims to `maxMessagesPerConversation` limit (default: 500) - Zero-config in-memory mode for development - `conversation_search` tool is automatically provided as a request-scoped tool whenever a `conversationId` is active **Memory model:** Agent memory is per-conversation by default. The `conversation_search` tool is bound at invocation time to the current conversation — the LLM cannot override this scope, and turns from other conversations are structurally unreachable. Use `knowledge_add` to promote durable facts that should persist across conversations; knowledge is the only cross-conversation bridge. ## Knowledge Integration Integrate knowledge bases for RAG (domain knowledge, not conversation history). Knowledge is configured at the SDK level and automatically available to all agents: ```typescript import { Toolpack } from 'toolpack-sdk'; import { Knowledge, MemoryProvider } from '@toolpack-sdk/knowledge'; // Configure knowledge at SDK level const knowledge = await Knowledge.create({ provider: new MemoryProvider(), }); const toolpack = await Toolpack.init({ provider: 'openai', knowledge, // Available to all agents using this toolpack }); class SmartAgent extends BaseAgent { async invokeAgent(input) { // Both `knowledge_search` and `knowledge_add` tools are // automatically available as request-scoped tools. // The AI can use them to retrieve or store information. const result = await this.run(input.message); return result; } } ``` **Available Tools:** - `knowledge_search` — Search the knowledge base for relevant information - `knowledge_add` — Add new information to the knowledge base at runtime The SDK automatically injects usage guidance into the system prompt when these tools are available. **Knowledge as the cross-conversation bridge:** `knowledge_add` is the *only* path by which information crosses conversation boundaries. Conversation history is scoped to the current conversation and inaccessible elsewhere; anything promoted via `knowledge_add` becomes available in all future conversations for that agent. Promote when: - A task surfaces a fact useful beyond the current conversation - A user states a durable preference - A decision is made that future conversations should respect Do **not** promote: - Routine task outputs (e.g., "answered a weather question") - Context that is specific to this conversation only - Confidential information whose visibility should remain inside the current conversation Because every promotion is an explicit agent action visible in traces, the knowledge base stays auditable and intentional. If you need per-entry visibility controls (e.g., scoping a knowledge entry to a subset of channels), that is a future extension — for now, apply developer discipline: only promote what every future conversation is permitted to see. ## Multi-Channel Routing Send output to multiple channels: ```typescript class MultiChannelAgent extends BaseAgent { async invokeAgent(input) { const result = await this.run(input.message); await this.sendTo('slack', result.output); await this.sendTo('email-team', result.output); await this.sendTo('sms-alerts', 'Task done!'); return result; } } ``` ## Agent Events Listen to agent lifecycle events: ```typescript const agent = new MyAgent(sdk); agent.on('agent:start', (input) => { console.log('Agent started:', input.message); }); agent.on('agent:complete', (result) => { console.log('Agent completed:', result.output); }); agent.on('agent:error', (error) => { console.error('Agent error:', error); }); ``` ## Hot Reload & Graceful Restart `HotReloadWatcher` watches your source files for changes and triggers a graceful restart once all in-flight conversations finish. The process exits cleanly (`process.exit(0)`) so a process manager (PM2, systemd) can bring it back up with the new `dist/` and `.env`. ### How it works 1. A file change is detected in a watched directory. 2. A 30-second debounce timer starts. Every new change resets it. 3. After 30 seconds of silence: - `.ts` / `.tsx` → runs `tsc --build`. On success, calls `onRestartNeeded`. - `.env*` → calls `onRestartNeeded` directly (no compile step). 4. `onRestartNeeded` calls `registry.scheduleRestart()`. 5. The registry waits for all active conversations to finish, then calls `process.exit(0)`. 6. The process manager restarts the process with fresh compiled output and environment variables. ### Setup ```typescript import { AgentRegistry, HotReloadWatcher } from '@toolpack-sdk/agents'; const registry = new AgentRegistry([myAgent]); await registry.start(); const watcher = new HotReloadWatcher({ watchPaths: ['./src'], // Directories or files to watch cwd: process.cwd(), // Working directory for tsc --build debounceMs: 30_000, // Wait 30s of silence before acting (default) onRestartNeeded: () => registry.scheduleRestart(), onCompileError: (msg) => console.error('[tsc]', msg), }); watcher.start(); ``` ### scheduleRestart options ```typescript registry.scheduleRestart({ maxWaitMinutes: 30, // Force restart after this many minutes even if conversations are still active (default: 30) }); ``` `scheduleRestart()` is **idempotent** — calling it multiple times (e.g., two files change within the same debounce window) has no effect after the first call. ### Persistent conversation history across restarts In-memory conversation history is lost when the process exits. Use `SQLiteConversationStore` from `toolpack-sdk` so history survives restarts. Requires `better-sqlite3`: ```bash npm install better-sqlite3 ``` ```typescript import { SQLiteConversationStore } from 'toolpack-sdk'; class MyAgent extends BaseAgent { name = 'my-agent'; description = 'My agent'; mode = 'chat'; conversationHistory = new SQLiteConversationStore({ dbPath: './conversations.db' }); async invokeAgent(input) { return this.run(input.message); } } ``` The SQLite file survives `process.exit(0)`. The new process re-opens the same file and picks up full conversation history — users continue mid-conversation as if nothing happened. ### AgentRegistry — dynamic agent management The registry supports adding and removing agents at runtime after `start()`: ```typescript // Add an agent after the registry is already running const newAgent = new ResearchAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); await registry.addAgent(newAgent); // wired + started immediately // Remove an agent by name (stops it and unregisters its channels) await registry.removeAgent('research-agent'); ``` ### Self-evolving agents If an agent uses a `CodingAgent` sub-agent to edit its own source files, the orchestrator holds a conversation lock for the duration of that task. The hot reload watcher detects the file changes and calls `scheduleRestart()` — but the restart only executes after the orchestrator's conversation finishes and the lock is released. The new code takes effect on the next PM2/systemd restart cycle. ## Extending Built-in Agents Customize built-in agents with your own prompts and logic: ```typescript import { ResearchAgent } from '@toolpack-sdk/agents'; import { AGENT_MODE } from 'toolpack-sdk'; class FintechResearchAgent extends ResearchAgent { mode = { ...AGENT_MODE, systemPrompt: 'You are a fintech research specialist. Always cite sources and flag regulatory implications.', }; async onComplete(result) { // Notify team await this.sendTo('slack-research', result.output); } } // Knowledge is configured at SDK level, not on the agent. // The AI can use `knowledge_add` to store information during execution. const toolpack = await Toolpack.init({ provider: 'openai', knowledge: await Knowledge.create({ provider: new MemoryProvider() }), }); ``` ## Peer Dependencies The following are optional peer dependencies. Install only what you need: ```bash # For DiscordChannel npm install discord.js # For EmailChannel npm install nodemailer # For SMSChannel npm install twilio ``` ## API Reference ### BaseAgent ```typescript abstract class BaseAgent { abstract name: string; abstract description: string; abstract mode: ModeConfig | string; // Core method to implement abstract invokeAgent(input: AgentInput): Promise; // Built-in methods protected run(message: string, options?: AgentRunOptions, context?: { conversationId?: string }): Promise; protected sendTo(channelName: string, message: string): Promise; protected ask(question: string, options?: { context?: Record; maxRetries?: number; expiresIn?: number }): Promise; protected getPendingAsk(conversationId?: string): PendingAsk | null; } ``` ### AgentRegistry ```typescript class AgentRegistry { constructor(agents: BaseAgent[]); start(): Promise; stop(): Promise; sendTo(channelName: string, output: AgentOutput): Promise; getAgent(name: string): AgentInstance | undefined; getChannel(name: string): ChannelInterface | undefined; invoke(agentName: string, input: AgentInput): Promise; // Dynamic agent management addAgent(agent: BaseAgent): Promise; // Wires + starts immediately if registry is already running removeAgent(name: string): Promise; // Stops the agent and unregisters its channels // Graceful restart isAllIdle(): boolean; // True when no agent has an active conversation scheduleRestart(options?: { maxWaitMinutes?: number }): void; // Idempotent; waits for idle then exits } ``` ### HotReloadWatcher ```typescript class HotReloadWatcher { constructor(options: HotReloadWatcherOptions); start(): void; stop(): void; } interface HotReloadWatcherOptions { watchPaths: string[]; // Directories or files to watch cwd?: string; // Working directory for tsc --build (default: process.cwd()) debounceMs?: number; // Silence window before acting (default: 30 000 ms) onRestartNeeded: () => void; // Called after a successful compile or an .env change onCompileError?: (stderr: string) => void; // Called when tsc --build exits non-zero (restart NOT triggered) spawnFn?: SpawnFn; // Inject a custom spawn function (testing) watchFn?: WatchFn; // Inject a custom watch function (testing) } ``` ### Channels All channels extend `BaseChannel`: ```typescript abstract class BaseChannel { abstract readonly isTriggerChannel: boolean; name?: string; abstract listen(): void; abstract send(output: AgentOutput): Promise; abstract normalize(incoming: unknown): AgentInput; onMessage(handler: (input: AgentInput) => Promise): void; } ``` ## Agent-to-Agent Messaging Agents can delegate tasks to other agents without tight coupling. ### Local Delegation (Same Process) ```typescript import { AgentRegistry, BaseAgent } from '@toolpack-sdk/agents'; import type { AgentInput, AgentResult } from '@toolpack-sdk/agents'; class EmailAgent extends BaseAgent { name = 'email-agent'; description = 'Sends email reports'; mode = 'chat'; channels = [slack]; // channels are class properties, not constructor args async invokeAgent(input: AgentInput): Promise { // Delegate to DataAgent and wait for result const report = await this.delegateAndWait('data-agent', { message: 'Generate weekly leads report', intent: 'generate_report', }); return { output: `Email sent with report: ${report.output}`, }; } } const emailAgent = new EmailAgent({ apiKey: process.env.ANTHROPIC_API_KEY! }); const dataAgent = new DataAgent({ apiKey: process.env.ANTHROPIC_API_KEY! }); const registry = new AgentRegistry([emailAgent, dataAgent]); await registry.start(); ``` ### Cross-Process Delegation (JSON-RPC) **Server (Host Agents):** ```typescript import { AgentJsonRpcServer } from '@toolpack-sdk/agents'; const server = new AgentJsonRpcServer({ port: 3000 }); server.registerAgent('data-agent', new DataAgent({ apiKey: process.env.ANTHROPIC_API_KEY! })); server.registerAgent('research-agent', new ResearchAgent({ apiKey: process.env.ANTHROPIC_API_KEY! })); server.listen(); ``` **Client (Call Remote Agents):** ```typescript import { AgentRegistry, JsonRpcTransport, BaseAgent } from '@toolpack-sdk/agents'; import type { AgentInput, AgentResult } from '@toolpack-sdk/agents'; const emailAgent = new EmailAgent({ apiKey: process.env.ANTHROPIC_API_KEY! }); const registry = new AgentRegistry([emailAgent], { transport: new JsonRpcTransport({ agents: { 'data-agent': 'http://localhost:3000', 'research-agent': 'http://remote-server:3000', } }) }); // Inside EmailAgent class EmailAgent extends BaseAgent { async invokeAgent(input: AgentInput): Promise { // Can now delegate to remote agents const report = await this.delegateAndWait('data-agent', { message: 'Generate report' }); return { output: `Email sent with: ${report.output}` }; } } ``` ### Delegation Methods - **`delegate(agentName, input)`** - Fire-and-forget, returns immediately - **`delegateAndWait(agentName, input)`** - Waits for result, returns `AgentResult` ## Registry Discover and publish community-built agents. ### Finding Agents ```typescript import { searchRegistry } from '@toolpack-sdk/agents/registry'; // Search all agents const results = await searchRegistry(); // Search by keyword const results = await searchRegistry({ keyword: 'fintech' }); // Filter by category const results = await searchRegistry({ category: 'research' }); // Display results for (const agent of results.agents) { console.log(`${agent.name}: ${agent.toolpack?.description || agent.description}`); console.log(` Install: npm install ${agent.name}`); } ``` ### Publishing an Agent Add the `toolpack` metadata to your `package.json`: ```json { "name": "toolpack-agent-fintech-research", "version": "1.0.0", "keywords": ["toolpack-agent"], "toolpack": { "agent": true, "category": "research", "description": "Research agent focused on fintech news and regulatory updates", "tags": ["fintech", "news", "research"] } } ``` Requirements: - Must include `"toolpack-agent"` in `keywords` - Must have `"toolpack": { "agent": true }` in package.json - Agent class must extend `BaseAgent` ## Error Handling ### Error Types | Error | Cause | Resolution | |-------|-------|------------| | `AgentError` | Generic agent failure | Check error message for details | | `AgentError` (delegate) | Agent not registered | Ensure agent is registered with `AgentRegistry` | | `AgentError` (transport) | Transport misconfiguration | Verify transport config and agent URLs | | `RegistryError` | NPM registry failure | Check network connection and registry URL | ### Handling Errors ```typescript import { AgentError } from '@toolpack-sdk/agents'; try { const result = await agent.invokeAgent({ message: 'Hello' }); } catch (error) { if (error instanceof AgentError) { // Agent-specific error console.error('Agent failed:', error.message); } else { // Unknown error console.error('Unexpected error:', error); } } ``` ### Common Issues **Agent not found during delegation** ``` Agent "data-agent" not found in registry. Available agents: email-agent, browser-agent ``` → Ensure the target agent is registered in `AgentRegistry`. **Transport configuration error** ``` No transport configured for delegation ``` → Use `AgentRegistry` with `LocalTransport` (default) or configure `JsonRpcTransport` for cross-process communication. **JSON-RPC connection failure** ``` Failed to invoke agent "data-agent" at http://localhost:3000: fetch failed ``` → Verify the JSON-RPC server is running and the URL/port is correct. ## Agent Spawning When `spawn` is configured on a `BaseAgent`, two tools are injected into every `run()` call: `spawn_agent` and `spawn_agents_parallel`. The LLM uses these to instantiate lightweight helper agents on-demand, get their results, and continue — all within a single conversation turn. Spawned agents are ephemeral: no channels, no registry entry, discarded after use. ```typescript import { BaseAgent } from '@toolpack-sdk/agents'; import type { AgentSpawnConfig } from '@toolpack-sdk/agents'; class OrchestratorAgent extends BaseAgent { name = 'orchestrator'; description = 'Coordinates research and coding tasks'; mode = 'agent'; spawn: AgentSpawnConfig = { enabled: true, templates: [ { name: 'researcher', description: 'Searches the web and summarises findings on a topic.', systemPrompt: (task) => `You are a focused research agent. Task: ${task}`, }, { name: 'coder', description: 'Writes or refactors code for a specific request.', systemPrompt: (task) => `You are a senior engineer. Task: ${task}`, model: 'claude-opus-4-8', }, ], maxDepth: 3, // spawned agents can themselves spawn, up to this depth }; async invokeAgent(input) { return this.run(input.message); } } ``` **Key options:** | Option | Type | Default | Description | |--------|------|---------|-------------| | `enabled` | `boolean` | — | Must be `true` for tools to be injected | | `templates` | `AgentSpawnTemplate[]` | — | Available spawn targets. LLM picks by `name` + `description`. | | `maxDepth` | `number` | `3` | Max recursive spawn depth. At the limit, spawn tools are silently omitted. | **Template options:** | Option | Type | Description | |--------|------|-------------| | `name` | `string` | Unique identifier. Use `'self'` for self-replication. | | `description` | `string` | Purpose shown to the LLM. | | `systemPrompt` | `(task: string) => string` | Called at spawn time with the task string. | | `model` | `string` | Model override. Inherits parent when omitted. | | `allowPromptAddition` | `boolean` | Allow LLM to append extra instructions via `systemPromptAddition`. Default: `false`. | **Parallel spawning** — the LLM can call `spawn_agents_parallel` with an array of tasks; all agents run via `Promise.all`. **Self-replication** — add `{ name: 'self', ... }` to the templates list. The replica inherits the parent's mode and system prompt with the template's `systemPrompt(task)` appended. ## Interceptors Interceptors are composable middleware that run before `invokeAgent`. They can filter, enrich, classify, or short-circuit incoming messages. All built-ins are opt-in — none run unless you explicitly list them. Import from the dedicated subpath: ```typescript import { createNoiseFilterInterceptor, createRateLimitInterceptor, createSelfFilterInterceptor, // ... } from '@toolpack-sdk/agents/interceptors'; ``` ### Writing a Custom Interceptor ```typescript import type { Interceptor } from '@toolpack-sdk/agents/interceptors'; const myInterceptor: Interceptor = async (input, ctx, next) => { if (shouldIgnore(input)) { return ctx.skip(); // End the chain silently — no reply sent } const result = await next(); // Continue to next interceptor or agent return result; }; class MyAgent extends BaseAgent { interceptors = [myInterceptor]; } ``` ### Registering Interceptors ```typescript import { createNoiseFilterInterceptor, createRateLimitInterceptor, } from '@toolpack-sdk/agents/interceptors'; class MyAgent extends BaseAgent { name = 'my-agent'; description = 'My agent'; mode = 'chat'; interceptors = [ createNoiseFilterInterceptor({ denySubtypes: ['message_changed', 'message_deleted'] }), createRateLimitInterceptor({ getKey: (input) => input.participant?.id ?? 'anon', tokensPerInterval: 5, interval: 60000, // 5 messages per minute per user }), ]; async invokeAgent(input) { return this.run(input.message); } } ``` ### Built-in Interceptors | Interceptor | Purpose | |---|---| | `createNoiseFilterInterceptor` | Drop messages by subtype (edits, deletes, bot messages) | | `createEventDedupInterceptor` | Drop duplicate events (Slack retries, webhook redeliveries) | | `createSelfFilterInterceptor` | Drop the agent's own messages (infinite loop guard) | | `createRateLimitInterceptor` | Token-bucket rate limiting per user or conversation | | `createAddressCheckInterceptor` | Rule-based address detection (@mention, vocative, direct message) | | `createIntentClassifierInterceptor` | LLM-based intent classification for ambiguous address checks | | `createParticipantResolverInterceptor` | Resolve participant identity from platform user ID | | `createCaptureInterceptor` | Persist inbound and outbound messages to conversation history (auto-registered) | | `createDepthGuardInterceptor` | Reject delegation chains that exceed a configured depth | | `createTracerInterceptor` | Structured logging of each chain hop for debugging | | `createOTelTracerInterceptor` | OpenTelemetry span per invocation — compatible with any OTel-compliant backend | ## Capabilities Capability agents are headless agents with no channels. They are invoked by interceptors or other agents for specific cross-cutting concerns. Import from the dedicated subpath: ```typescript import { IntentClassifierAgent, SummarizerAgent } from '@toolpack-sdk/agents/capabilities'; ``` ### IntentClassifierAgent Classifies whether a message is directly addressing the target agent. Used by `createIntentClassifierInterceptor` to resolve ambiguous cases that rules alone cannot determine. ```typescript import { IntentClassifierAgent } from '@toolpack-sdk/agents/capabilities'; import type { IntentClassifierInput } from '@toolpack-sdk/agents/capabilities'; const classifier = new IntentClassifierAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); const result = await classifier.invokeAgent({ message: 'classify', data: { message: 'Hey @assistant can you help?', agentName: 'assistant', agentId: 'U123', senderName: 'alice', channelName: 'general', } as IntentClassifierInput, }); // result.output === 'direct' | 'indirect' | 'passive' | 'ignore' ``` ### SummarizerAgent Compresses older conversation history turns into a compact summary. Used by the prompt assembler when conversation history exceeds the token budget. ```typescript import { SummarizerAgent } from '@toolpack-sdk/agents/capabilities'; import type { SummarizerInput, SummarizerOutput } from '@toolpack-sdk/agents/capabilities'; const summarizer = new SummarizerAgent({ apiKey: process.env.ANTHROPIC_API_KEY }); const result = await summarizer.invokeAgent({ message: 'summarize', data: { turns: olderTurns, agentName: 'support-agent', agentId: 'U123', maxTokens: 500, extractDecisions: true, } as SummarizerInput, }); const summary = JSON.parse(result.output) as SummarizerOutput; ``` ## Evals — LLM Quality Evaluation Unit tests verify wiring; evals verify agent **quality**. Use the eval primitives to build regression suites and track answer quality over time. ```typescript import { EvalDataset, EvalRunner, ContainsScorer, LLMJudgeScorer, compareEvalRuns, formatEvalReport, } from '@toolpack-sdk/agents'; const dataset = new EvalDataset([ { id: 'q1', input: 'What is 2+2?', expectedOutput: '4' }, { id: 'q2', input: 'Capital of France?', expectedOutput: 'Paris' }, ]); const runner = new EvalRunner({ agent: myAgent, dataset, scorers: [new ContainsScorer()], }); const run = await runner.run(); console.log(`Average score: ${(run.averageScore * 100).toFixed(1)}%`); ``` **Four built-in scorers:** | Scorer | When to use | |---|---| | `ExactMatchScorer` | Deterministic outputs — exact string match | | `ContainsScorer` | Output must contain the expected string | | `LLMJudgeScorer` | Open-ended answers — ask an LLM to grade on 0–1 | | `CustomScorer` | Any custom scoring logic | **Regression detection:** ```typescript const report = compareEvalRuns(baselineRun, currentRun); console.log(formatEvalReport(report)); expect(report.regressions).toHaveLength(0); // CI gate ``` ## Testing ```bash npm test ``` ## License Apache 2.0 © Toolpack SDK