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

armorer

v0.8.2

Published

A lightweight registry for validated AI tools. Build tools with Zod schemas and metadata, register them in a toolbox, and execute/query them with event hooks.

Readme

Armorer

armorer is the Agent Bureau tool layer. It provides type-safe tool definitions, validated execution, toolbox registries, provider adapters, middleware, MCP integration, and testing utilities.

Table of Contents

Overview

Toolbox turns tool calling into a structured, observable, and searchable workflow. Define schemas once, validate at runtime, and export tools to popular providers without rewriting adapters.

What It Does

  • Creates validated tools with createTool().
  • Groups tools into immutable registries with createToolbox().
  • Executes tool calls with runtime validation, middleware, events, policies, idempotency, and streaming support.
  • Converts tool definitions and tool results for OpenAI, Anthropic, Gemini, OpenAI Agents SDK, and MCP.
  • Supports registry querying, semantic search, inspection, truncation, and testing utilities.

How It Works

A tool combines metadata, an input schema, and an execute function. A toolbox registers one or more tools, validates incoming calls, runs middleware and policy hooks, executes the matching tool, and emits typed lifecycle events. Provider adapters materialize the same toolbox into provider-specific tool declarations and parse provider tool calls back into the shared interoperability contract.

Project Role

armorer is the action layer for Agent Bureau. operative uses it to execute tool calls during an agent run, herald uses its adapters when sending tools to providers, skills and memory expose their capabilities as toolboxes, and gateway surfaces the composed tool set through API and UI state.

Features

  • Zod-powered schema validation with TypeScript inference
  • Central tool registry with execution, policy, and event hooks
  • Query helpers with text, tag, schema, and metadata filters
  • Semantic search with vector embeddings (OpenAI, Pinecone, etc.)
  • Provider adapters for OpenAI, Anthropic, and Gemini
  • Tool composition utilities (pipe/bind/when/parallel/retry)
  • OpenTelemetry Instrumentation: Native tracing for agentic loops
  • Built-in Middleware: Caching, Rate Limiting, and Timeouts
  • Testing Utilities: Mock tools and test registries for easy verification
  • MCP server integration for exposing tools over MCP
  • OpenAI Agents SDK integration with tool gating and MCP support for Claude Agent SDK
  • Concurrency controls and execution tracing hooks
  • Pre-configured search tool for semantic tool discovery in agentic workflows

Package Structure

Toolbox is organized into focused submodules so you can import only what you need:

Core Modules

armorer (Main Entry Point)

The primary API for creating and managing tools:

import { createToolbox, createTool, isTool } from 'armorer';

Exports: createToolbox, createTool, createToolCall, combineToolboxes (plus deprecated alias combineToolbox), lazy, withContext, isTool, isToolbox, createMiddleware, provider import helpers on createToolbox, and all core types.

armorer/utilities

Composition and utility functions:

import { pipe, parallel, retry, when } from 'armorer/utilities';

Exports: Everything from main entry point plus pipe, bind, parallel, retry, when, tap, preprocess, postprocess, PipelineError, error utilities, and composition types.

armorer/query

Query helpers and predicates for filtering tools:

import { queryTools, textMatches, tagsMatchAll, schemaMatches } from 'armorer/query';

Exports: queryTools, reindexSearchIndex, textMatches, tagsMatchAll, tagsMatchAny, tagsMatchNone, schemaMatches, schemaHasKeys, and related types.

armorer/inspect

Tool and registry inspection utilities:

import { inspectTool, inspectRegistry } from 'armorer/inspect';

Exports: inspectTool, inspectRegistry, extractSchemaSummary, extractMetadataFlags, and Zod schemas for inspection results.

Provider Adapters

armorer/adapters/openai

OpenAI Chat Completions API format:

import {
  formatOpenAIToolResults,
  formatOpenAIToolResultsAsync,
  fromOpenAITools,
  parseOpenAIToolCalls,
  toOpenAITools,
} from 'armorer/adapters/openai';

armorer/adapters/anthropic

Anthropic Messages API format:

import {
  formatAnthropicToolResults,
  fromAnthropicTools,
  parseAnthropicToolCalls,
  toAnthropicTools,
} from 'armorer/adapters/anthropic';

armorer/adapters/gemini

Google Gemini API format:

import {
  formatGeminiToolResults,
  fromGeminiTools,
  parseGeminiToolCalls,
  toGeminiTools,
} from 'armorer/adapters/gemini';

Toolboxes also expose lazy provider exporters, and createToolbox exposes matching lazy provider import helpers:

const openAITools = await toolbox.toOpenAITools();
const importedToolbox = await createToolbox.fromOpenAITools(openAITools, {
  getTool(configuration) {
    return async (params) => {
      throw new Error(`Add execute for ${configuration.name}`);
    };
  },
});

armorer/truncation

Truncation utilities for tool results:

import { truncateToolResultContent, containsBase64Data } from 'armorer/truncation';

Exports: truncateToolResultContent, truncateToolResultContentStructured, truncateText, safeSlice, createTruncatingAsyncIterable, containsBase64Data, stripBase64Data, isHighSurrogate, isLowSurrogate, DEFAULT_MAX_CHARACTERS, DEFAULT_ERROR_MAX_CHARACTERS, and types TruncationOptions, ToolResultTruncationOptions, StructuredToolResultTruncationOptions, and StructuredToolResultTruncation.

armorer/idempotency

Persistent idempotency helpers for at-least-once executors:

import { createToolResultCache, fullInputKey, withToolboxIdempotency } from 'armorer/idempotency';

Exports: createToolResultCache, withIdempotency, withToolboxIdempotency, fullInputKey, fieldKey, compositeKey, namespacedKey, and idempotency cache/result types.

Infrastructure

armorer/instrumentation

OpenTelemetry tracing:

import { instrument } from 'armorer/instrumentation';

armorer/middleware

Standard middleware (caching, rate limiting, timeouts, truncation):

import {
  createCacheMiddleware,
  createRateLimitMiddleware,
  createTimeoutMiddleware,
  createTruncationMiddleware,
} from 'armorer/middleware';

armorer/test

Testing utilities:

import { createMockTool, createTestRegistry } from 'armorer/test';

Integrations

armorer/mcp (or armorer/integrations/mcp)

Model Context Protocol server integration:

import { createMCP, toMcpTools, fromMcpTools } from 'armorer/mcp';

armorer/adapters/open-ai/agents

OpenAI Agents SDK integration with tool gating:

import { toOpenAIAgentTools, createOpenAIToolGate } from 'armorer/adapters/open-ai/agents';

Other Utilities

armorer/tools

Pre-built tools (search, etc.):

import { createSearchTool } from 'armorer/tools';

armorer/utilities

Composition utilities (re-exported from armorer/utilities):

import { pipe, bind, parallel } from 'armorer/utilities';

Quick Start

import { createToolbox, createTool } from 'armorer';
import { z } from 'zod';

const addNumbers = createTool({
  name: 'add-numbers',
  description: 'Add two numbers together',
  input: z.object({
    a: z.number(),
    b: z.number(),
  }),
  tags: ['math', 'calculator'],
  async execute({ a, b }) {
    return a + b;
  },
});

const toolbox = createToolbox([addNumbers]);

const toolCall = await toolbox.execute({
  id: 'call-123',
  name: 'add-numbers',
  arguments: { a: 5, b: 3 },
});

console.log(toolCall.result); // 8

Immutable Toolbox Composition

Compose toolboxes without mutating existing instances.

import { createToolbox, combineToolboxes } from 'armorer';

const base = createToolbox([mathTool], {
  context: { region: 'us-east-1' },
});

const extended = base.extend(stringTool);
// `base` is unchanged, `extended` has both tools.

const adminTools = createToolbox([auditTool], {
  context: { role: 'admin' },
});

const merged = base.extend(adminTools);
// Context is shallow merged, last toolbox wins:
// merged context => { region: 'us-east-1', role: 'admin' }

const combined = combineToolboxes(base, adminTools);
// Same merge rules, useful when combining many toolboxes at once.

Safety and Policy

Use policy hooks to block or gate risky actions before execution.

Batch Execution

Execute multiple tools in parallel or sequentially with global controls.

const results = await toolbox.execute([call1, call2, call3], {
  concurrency: 5, // Global concurrency limit
  mode: 'parallel', // 'parallel' | 'sequential'
  errorMode: 'collect', // 'collect' (default) | 'failFast'
});

Approval Flows

Policies can pause execution for human approval or input.

const toolbox = createToolbox([], {
  // Use the same secret anywhere pending approvals may be resumed.
  approvalSecret: process.env.ARMORER_APPROVAL_SECRET,
  policy: {
    async beforeExecute(context) {
      if (context.metadata?.sensitive) {
        return {
          status: 'needs_approval',
          reason: 'Sensitive action requires confirmation',
        };
      }
      return { allow: true };
    },
  },
});

const result = await toolbox.execute(sensitiveCall);
if (result.outcome === 'action_required') {
  // Persist the descriptor and show the approval UI to the user.
  await durableStore.set(result.pendingApproval!.callId, result.pendingApproval);
}

// Later, possibly in another process, rebuild the same toolbox and resume.
const approved = await durableStore.get('tool-call-id');
const resumed = await toolbox.resumeApproval(approved);

if (resumed.executedArgumentsEdited) {
  // Record both the proposed and executed arguments in your transcript.
}

pendingApproval is JSON-serializable and includes callId, toolName, validated arguments, the requested action, reason, and tool metadata. When the toolbox has an approvalSecret, the descriptor also includes an approvalToken and can be treated as SignedPendingToolApproval for resumeApproval(). Use the same approvalSecret anywhere the approval may be resumed. Without approvalSecret, pending approvals are unsigned and cannot be resumed through resumeApproval(). resumeApproval() verifies the token, reconstructs the original call id, re-validates the original or edited arguments, and re-runs policy against those arguments. The granted approval only satisfies the original approval prompt when the resumed arguments match the proposed arguments; edited arguments must be allowed by policy. The result sets executedArgumentsEdited when the resumed arguments differ from the proposed arguments.

Agent Integration

Toolbox provides helpers to integrate with large language model providers like OpenAI.

import {
  formatOpenAIToolResults,
  formatOpenAIToolResultsAsync,
  parseOpenAIToolCalls,
  toOpenAITools,
} from 'armorer/adapters/openai';

// 1. Export tools
const tools = toOpenAITools(toolbox);

// 2. Call model
const completion = await openai.chat.completions.create({ tools, ... });

// 3. Parse and execute
const toolCalls = parseOpenAIToolCalls(completion.choices[0].message.tool_calls);
const results = await toolbox.execute(toolCalls);

// 4. Format results
const messages = formatOpenAIToolResults(results);

// Use async formatter when any tool call uses { stream: true }
const streamingMessages = await formatOpenAIToolResultsAsync(results);

If you want the root package to stay adapter-light until you need it, use the lazy toolbox methods instead:

const tools = await toolbox.toOpenAITools();
const imported = await createToolbox.fromOpenAITools(tools, {
  getTool(configuration) {
    return async (params) => loadExecute(configuration.name, params);
  },
});

Using with Conversationalist

Use armorer for tool schemas, provider tool definitions, tool-call parsing, and execution. Use conversationalist for the persistent conversation state and provider message history.

import {
  appendToolCalls,
  appendToolResultsAsync,
  appendUserMessage,
  createConversationHistory,
} from 'conversationalist/conversation';
import { toOpenAIMessagesGrouped } from 'conversationalist/adapters/openai';
import { createToolbox } from 'armorer';
import { parseOpenAIToolCalls, toOpenAITools } from 'armorer/adapters/openai';

let conversation = createConversationHistory({ title: 'Weather' });
conversation = appendUserMessage(conversation, 'What is the weather in Denver?');

const tools = toOpenAITools(toolbox);
const messages = toOpenAIMessagesGrouped(conversation);
const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages, tools });

const toolCalls = parseOpenAIToolCalls(completion.choices[0]?.message?.tool_calls);
conversation = appendToolCalls(conversation, toolCalls);

const results = await toolbox.execute(toolCalls, { stream: true });
conversation = await appendToolResultsAsync(conversation, results);

See Using armorer with conversationalist for complete OpenAI, Anthropic, and Gemini examples.

Observability (OpenTelemetry)

Native instrumentation for distributed tracing.

import { createToolbox } from 'armorer';
import { instrument } from 'armorer/instrumentation';
import { context, trace } from '@opentelemetry/api';

const toolbox = createToolbox();
instrument(toolbox); // Auto-wires all tool calls to OpenTelemetry spans

const tracer = trace.getTracer('worker');
await tracer.startActiveSpan('temporal.activity', async (activitySpan) => {
  await toolbox.execute(
    { id: 'lookup-account', name: 'lookupAccount', arguments: { accountId: 'acct_123' } },
    {
      parentContext: trace.setSpan(context.active(), activitySpan),
      spanLinks: [{ context: activitySpan.spanContext() }],
    },
  );
});

Pass parentContext to nest tool spans under an existing OpenTelemetry context. Pass links to attach span links when the orchestrator prefers linked traces over a direct parent-child relationship.

Middleware

Batteries-included middleware for production needs.

import { createToolbox } from 'armorer';
import {
  createCacheMiddleware,
  createRateLimitMiddleware,
  createTruncationMiddleware,
} from 'armorer/middleware';

const toolbox = createToolbox([], {
  middleware: [
    createCacheMiddleware({ ttlMs: 60000 }),
    createRateLimitMiddleware({ limit: 100, windowMs: 60000 }),
    createTruncationMiddleware({ maxCharacters: 2000 }),
  ],
});

Truncation

Prevent oversized tool results from blowing up context windows. The truncation utilities safely handle UTF-16 surrogate pairs and strip base64 data. When a tool returns a streaming result (an object with stream or result fields containing an AsyncIterable), the middleware wraps the streams so chunks are yielded until the character limit is reached, then a truncation marker is emitted and iteration stops.

import { truncateToolResultContent } from 'armorer/truncation';
import { createTruncationMiddleware } from 'armorer/middleware';

// Standalone usage
const truncated = truncateToolResultContent(longResult, {
  maxCharacters: 4000,
  isError: false,
});

// As middleware (handles both string results and streaming results)
const toolbox = createToolbox(tools, {
  middleware: [createTruncationMiddleware({ maxCharacters: 4000 })],
});

For large tool outputs that are stored separately, use the structured head-and-tail helper:

import { truncateToolResultContentStructured } from 'armorer/truncation';

const excerpt = truncateToolResultContentStructured(toolOutput, {
  maxBytes: 8000,
});

if (excerpt.truncated) {
  console.log(`${excerpt.head}\n... omitted ${excerpt.omittedBytes} bytes ...\n${excerpt.tail}`);
}

The structured result is { head, tail, originalSize, omittedBytes, truncated }. Byte counts use UTF-8 size after base64 payload replacement, and the excerpts avoid splitting UTF-16 surrogate pairs.

Idempotency

Use armorer/idempotency when a tool might be retried by an at-least-once executor. The cache records three outcomes:

  • fresh: this process executed the tool and recorded the result.
  • deduped: a completed result already existed and was returned.
  • unknown-outcome: execution started earlier, but no result was recorded.
import { createToolbox } from 'armorer';
import { createToolResultCache, withToolboxIdempotency } from 'armorer/idempotency';

const cache = createToolResultCache({ store: durableKeyValueStore });
const toolbox = createToolbox([chargeCardTool]);
const idempotentToolbox = withToolboxIdempotency(toolbox, { cache });

const result = await idempotentToolbox.execute(
  {
    id: 'provider-call-1',
    name: 'charge-card',
    arguments: { cents: 2500 },
  },
  {
    idempotencyKey: temporalToolCallId,
  },
);

if (result.idempotency?.outcome === 'unknown-outcome') {
  // Do not replay blindly. Ask for review before retrying.
}

After human review, retry an unknown outcome explicitly:

await idempotentToolbox.execute(call, {
  idempotencyKey: temporalToolCallId,
  retryUnknownOutcome: true,
});

If you do not pass idempotencyKey, withToolboxIdempotency() uses each tool's configured idempotencyKey function. Tools without an idempotencyKey are not deduped by default; set requireExplicitKey: false to use fullInputKey for those tools. Caller-supplied keys are useful when an orchestrator already mints a stable key per provider tool_call_id and needs retries to reuse that exact key. Armorer scopes the cache entry by tool name, so the same external key cannot dedupe two different tools into one result.

The default createToolResultCache() serializes claimStarted() calls for the same key within a cache instance. For durable stores shared across processes, implement ToolResultCache.claimStarted() with compare-and-set semantics. Without that method, Armorer keeps compatibility with the original completed-result cache shape and falls back to a read-then-mark path when markStarted() is available. That fallback protects retries after a crashed execution, but only an atomic cache backend can prevent two concurrent processes from claiming the same new key at exactly the same time.

Fuzzy Tool Name Resolution

LLMs sometimes mangle tool names (wrong case, dots instead of hyphens). Enable resolution to auto-correct:

const toolbox = createToolbox(tools, {
  resolution: true,
});

toolbox.addEventListener('name-resolved', (event) => {
  console.log(
    `Resolved ${event.detail.originalName} → ${event.detail.resolvedName} (${event.detail.tier})`,
  );
});

Resolution tiers (in order): exact → case-insensitive → normalized (dot/slash/underscore → hyphen) → suffix (last segment). Ambiguous matches return not-found for safety.

Loop Detection

Catch stuck models that repeat the same tool call in a loop:

const toolbox = createToolbox(tools, {
  loopDetection: true, // or { warningThreshold: 5, blockThreshold: 10 }
});

toolbox.addEventListener('loop-warning', (event) => {
  console.warn(event.detail.message);
});

toolbox.addEventListener('loop-blocked', (event) => {
  console.error(event.detail.message);
  // Tool call was blocked and returned an error result
});

Detectors: simple repeat (same call N times) and ping-pong (alternating between two calls).

Testing

Utilities for testing tools and agent logic.

import { createMockTool, createTestRegistry } from 'armorer/test';

const mock = createMockTool({ name: 'weather' });
mock.mockResolve({ temp: 72 });

const toolbox = createTestRegistry();
toolbox.register(mock);

await toolbox.execute({ name: 'weather', arguments: {} });
console.log(toolbox.history[0].call.name); // 'weather'

Safety, Policy, and Metadata

Toolbox supports registry-level policy hooks and per-tool policy for centralized guardrails. You can also tag tools as mutating or read-only and enforce those tags at the registry. See the Registry documentation for details on querying, searching, and middleware.

import { createToolbox, createTool } from 'armorer';
import { z } from 'zod';

const toolbox = createToolbox([], {
  readOnly: true,
  policy: {
    beforeExecute({ toolName, metadata }) {
      if (metadata?.mutates) {
        return { allow: false, reason: `${toolName} is mutating` };
      }
    },
  },
  telemetry: true,
});

const writeFile = createTool({
  name: 'fs.write',
  description: 'Write a file',
  input: z.object({ path: z.string(), content: z.string() }),
  metadata: { mutates: true },
  async execute() {
    return { ok: true };
  },
});

toolbox.register(writeFile);

Metadata keys with built-in enforcement:

  • metadata.mutates: true marks a tool as mutating
  • metadata.readOnly: true marks a tool as read-only
  • metadata.dangerous: true marks a tool as dangerous
  • metadata.concurrency: number sets a per-tool concurrency limit

Registry options for enforcement:

  • readOnly: true denies mutating tools automatically
  • allowMutation: false denies mutating tools automatically
  • allowDangerous: false denies dangerous tools automatically

Execution tracing events (opt-in via telemetry: true):

  • tool.started with startedAt
  • tool.finished with status and durationMs

Per-tool concurrency:

createTool({
  name: 'git.status',
  description: 'status',
  metadata: { concurrency: 1 },
  input: z.object({}),
  async execute() {
    return { ok: true };
  },
});

Creating Tools

Overview

Define tools with Zod schemas, validation, and typed execution contexts. For advanced patterns like chaining tools together, see Tool Composition.

Basic Tool

const greetUser = createTool({
  name: 'greet-user',
  description: 'Greet a user by name',
  input: z.object({
    name: z.string(),
    formal: z.boolean().optional(),
  }),
  async execute({ name, formal }) {
    return formal ? `Good day, ${name}.` : `Hey ${name}!`;
  },
});

Tools are callable. await tool(params) and await tool.execute(params) are equivalent. If you need a ToolResult object instead of throwing on errors, use tool.execute(toolCall) or tool.executeWith(...).

executeWith(...) lets you supply params plus callId, timeout (milliseconds), signal, and stream in a single call, returning a ToolResult instead of throwing. rawExecute(...) invokes the underlying implementation with a full ToolContext when you need precise control over dispatch/meta or to bypass the ToolCall wrapper.

Tool schemas must be object schemas (z.object(...) or a plain object shape). Tool calls always pass a JSON object for arguments, so wrap primitives inside an object (for example, z.object({ value: z.number() })).

You can use isTool(obj) to check if an object is a tool:

import { isTool, createTool } from 'armorer';

const tool = createTool({ ... });
if (isTool(tool)) {
  // TypeScript knows tool is ToolboxTool here
  console.log(tool.name);
}

Creating and Registering in One Step

You can create a tool and register it with a toolbox in one step by passing the toolbox as the second argument:

const toolbox = createToolbox([], {
  context: { userId: 'user-123', apiKey: 'secret' },
});

const tool = createTool(
  {
    name: 'my-tool',
    description: 'A tool with toolbox context',
    input: z.object({ input: z.string() }),
    async execute({ input }, context) {
      // context includes toolbox.context automatically
      console.log('User:', context.userId);
      return input.toUpperCase();
    },
  },
  toolbox, // Automatically registers the tool
);

Tool Without Inputs

If your tool accepts no input arguments, omit input (it defaults to z.object({})):

const healthCheck = createTool({
  name: 'health-check',
  description: 'Verify service is alive',
  async execute() {
    return 'ok';
  },
});

Tool with Metadata

Metadata is a lightweight, out-of-band descriptor for things that should not be part of the tool's input schema. It is useful for discovery and routing (filter/query by tier, cost, capabilities, auth requirements), for UI grouping, or for analytics and policy checks without changing the tool signature.

const fetchWeather = createTool({
  name: 'fetch-weather',
  description: 'Get current weather for a location',
  input: z.object({
    city: z.string(),
    units: z.enum(['celsius', 'fahrenheit']).optional(),
  }),
  tags: ['weather', 'api', 'external'],
  metadata: {
    requiresAuth: true,
    rateLimit: 100,
    capabilities: ['read'],
  },
  async execute({ city, units = 'celsius' }) {
    // ... fetch weather data
    return { temp: 22, conditions: 'sunny' };
  },
});

Tool with Context

Use withContext to inject shared context into tools:

const createToolWithContext = withContext({ userId: 'user-123', apiKey: 'secret' });

const userTool = createToolWithContext({
  name: 'get-user-data',
  description: 'Fetch user data',
  input: z.object({}),
  async execute(_params, context) {
    // Access context.userId and context.apiKey
    return { userId: context.userId };
  },
});

Lazy-Loaded Execute Functions

You can supply execute as a promise that resolves to a function. To avoid import() starting immediately, wrap the dynamic import with lazy so it only loads on first execution:

import { lazy } from 'armorer/lazy';

const heavyTool = createTool({
  name: 'heavy-tool',
  description: 'Runs an expensive workflow',
  input: z.object({ input: z.string() }),
  execute: lazy(() => import('./tools/heavy-tool').then((mod) => mod.execute)),
});

If the promise rejects or resolves to a non-function, tool.execute(toolCall) returns a ToolResult with error set, and tool.execute(params) or calling the tool directly throws an Error with the same message.

Tool Events

Listen to tool execution lifecycle events:

const tool = createTool({
  name: 'my-tool',
  description: 'A tool with events',
  input: z.object({ input: z.string() }),
  async execute({ input }, { dispatch }) {
    dispatch({ type: 'progress', detail: { percent: 50, message: 'Processing...' } });
    return input.toUpperCase();
  },
});

tool.addEventListener('execute-start', (event) => {
  console.log('Starting:', event.detail.params);
});

tool.addEventListener('execute-success', (event) => {
  console.log('Result:', event.detail.result);
});

tool.addEventListener('execute-error', (event) => {
  console.error('Error:', event.detail.error);
});

tool.addEventListener('progress', (event) => {
  if (event.detail.percent !== undefined) {
    console.log(`${event.detail.percent}%: ${event.detail.message ?? ''}`);
  } else {
    console.log(event.detail.message ?? 'Progress update');
  }
});

Streaming Output

Tools that return an AsyncIterable support two execution modes:

  • default (stream omitted/false): Armorer collects chunks into an array and returns that array as result.
  • stream: true: Armorer returns a live stream on ToolResult.stream (and ToolResult.result), and you consume it incrementally.
const streamTool = createTool({
  name: 'stream-tool',
  description: 'Emits tokens',
  input: z.object({}),
  async execute() {
    return {
      async *[Symbol.asyncIterator]() {
        yield 'hello';
        yield 'world';
      },
    };
  },
});

// Collect fallback (default)
const collected = await streamTool.execute({
  id: 'collect-1',
  name: 'stream-tool',
  arguments: {},
});
console.log(collected.result); // ['hello', 'world']

// Live stream mode
const live = await streamTool.execute(
  { id: 'live-1', name: 'stream-tool', arguments: {} },
  { stream: true },
);

for await (const chunk of live.stream!) {
  console.log('chunk', chunk);
}

Stream lifecycle events are emitted for both modes: stream-start, stream-chunk, stream-end, and stream-error. output-chunk continues to be emitted for compatibility.

Dispatching Progress Events

To report progress from inside a tool, use the dispatch function provided in the ToolContext (second argument to execute). Emit a progress event with an optional percent number (0–100) and an optional message:

const longTask = createTool({
  name: 'long-task',
  description: 'Does work in phases',
  input: z.object({ input: z.string() }),
  async execute({ input }, { dispatch }) {
    dispatch({ type: 'progress', detail: { percent: 10, message: 'Queued' } });
    // ... do work
    dispatch({ type: 'progress', detail: { percent: 50, message: 'Halfway' } });
    // ... do more work
    dispatch({ type: 'progress', detail: { percent: 100, message: 'Done' } });
    return input.toUpperCase();
  },
});

Then subscribe to progress on the tool:

longTask.addEventListener('progress', (event) => {
  console.log(`${event.detail.percent}%: ${event.detail.message ?? ''}`);
});

Search Tool for Agentic Workflows

Toolbox includes a pre-configured search tool that lets agents discover available tools dynamically. This is useful when you have many tools and want the large language model to find the right one for a task.

import { createToolbox, createTool } from 'armorer';
import { createSearchTool } from 'armorer/tools';
import { z } from 'zod';

const toolbox = createToolbox();

// Install the search tool - it auto-registers with the toolbox
createSearchTool(toolbox);

// Register your tools (can be done before or after the search tool)
createTool(
  {
    name: 'send-email',
    description: 'Send an email to recipients',
    input: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
    tags: ['communication'],
    async execute({ to, subject, body }) {
      return { sent: true };
    },
  },
  toolbox,
);

// Agents can now search for tools via toolbox.execute()
const result = await toolbox.execute({
  name: 'search-tools',
  arguments: { query: 'contact someone' },
});

console.log(result.result);
// [{ name: 'send-email', description: '...', tags: ['communication'], score: 1.5 }]

The search tool:

  • Auto-registers with the toolbox when created
  • Discovers tools dynamically - finds tools registered before or after it
  • Works with provider adapters - included in toOpenAITools(toolbox), toAnthropicTools(toolbox), and toGeminiTools(toolbox)
  • Supports semantic search when embeddings are configured on the toolbox

See Search Tool documentation for filtering by tags, configuration options, and agentic workflow examples.

TypeScript

Overview

TypeScript inference guidance and type-level patterns. For a complete list of exported types, see the API Reference.

Toolbox is written in TypeScript and provides full type inference:

const tool = createTool({
  name: 'typed-tool',
  description: 'A typed tool',
  input: z.object({
    count: z.number(),
    name: z.string().optional(),
  }),
  async execute(params) {
    // params is typed as { count: number; name?: string }
    return params.count * 2;
  },
});

// Return type is inferred
const result = await tool({ count: 5 }); // number

Documentation

Longer-form docs live in documentation/:

Roadmap

See the workspace roadmap for planned features, community requests, and version goals.

License

MIT. See LICENSE.