Skip to main content

Tool interface reference

The tool contract: what a tool must provide, how its results are shaped, and the reducer pipeline that trims output before it enters the context window.

Source

packages/types/src/tool.ts and packages/types/src/tool-reducer.ts. Re-exported from @ethosagent/types.

ToolResult

Signature

import type { ToolResult } from '@ethosagent/types';

export type ToolResult =
| {
ok: true;
value: string;
structured?: Record<string, unknown>;
cost_usd?: number;
}
| {
ok: false;
error: string;
code: 'input_invalid' | 'not_available' | 'execution_failed' | 'STALE_WRITE';
};

Success variant

FieldTypeDescription
oktrueDiscriminant.
valuestringHuman/LLM-readable string. Always present. Post-trimmed against the per-call budget. Multimodal or structured-data tools use this as a concise text summary so the LLM can react without parsing JSON.
structuredRecord<string, unknown> | undefinedOptional structured payload for non-string results (image bytes as base64, tabular data, JSON documents, multi-part content). Consumers that do not know a tool's specific structured shape should ignore this field; value carries the authoritative summary.
cost_usdnumber | undefinedDollar cost attributed to this call (paid APIs, sandbox time). Surfaced in usage telemetry.

Error variant

FieldTypeDescription
okfalseDiscriminant.
errorstringHuman-readable error message. Goes back to the LLM verbatim.
codeerror codeStable error class (see table below).

Error codes

CodeMeaning
input_invalidThe LLM produced args that fail validation.
not_availableThe tool is gated off (missing API key, binary, or personality allowlist).
execution_failedRuntime failure inside execute.
STALE_WRITEThe file's on-disk mtime differs from the value recorded at read time; the write is refused to prevent silent clobber.

Notes

  • A tool that throws is automatically converted into { ok: false, code: 'execution_failed', error: err.message } by ToolRegistry.executeParallel.
  • Always return a ToolResult even on partial success -- encode the partial result in value and explain what worked. The LLM cannot recover from a thrown exception.
  • The [truncated -- N chars total] marker appended by the registry is part of value. Test fixtures should expect it.

ToolProgressEvent

Signature

import type { ToolProgressEvent } from '@ethosagent/types';

export interface ToolProgressEvent {
type: 'progress';
toolName: string;
message: string;
percent?: number;
audience?: 'internal' | 'user' | 'dashboard';
}

Members

FieldTypeDescription
type'progress'Literal discriminant.
toolNamestringName of the tool emitting the event.
messagestringHuman-readable progress description.
percentnumber | undefinedOptional 0--100 completion percentage.
audience'internal' | 'user' | 'dashboard'Controls who sees the event. 'internal' (default when absent): framework only (logs, telemetry, dev TUI). 'user': surfaced in the user-visible stream. 'dashboard': surfaced on operator dashboards but not to end users. See audience boundary.

Notes

  • Channel adapters (telegram, discord, slack, whatsapp, email) and apps/ethos/src/commands/chat.ts must not surface 'internal' events.
  • Use 'user' sparingly: long-running operations where silent latency would confuse the user (read_file reading >1 MB, multi-step bash commands).
  • The framework never opts in for the tool -- audience is always a per-event decision by the tool author.

ToolContext

Signature

import type { ToolContext, ToolProgressEvent } from '@ethosagent/types';

export interface ToolContext {
sessionId: string;
sessionKey: string;
platform: string;
workingDir: string;
agentId?: string;
personalityId?: string;
memoryScope?: 'global' | 'per-personality';
memoryScopeId?: string;
teamId?: string;
currentTurn: number;
messageCount: number;
abortSignal: AbortSignal;
emit: (event: ToolProgressEvent) => void;
resultBudgetChars: number;
storage?: import('@ethosagent/types').Storage;
readMtimes?: Map<string, { mtimeMs: number; readAtTurn: number }>;
networkPolicy?: {
allow?: string[];
deny?: string[];
allow_private_urls?: boolean;
};
kvStore?: import('@ethosagent/types').KeyValueStore;
secretsResolver?: import('@ethosagent/types').ScopedSecretsResolver;
scopedFetch?: import('@ethosagent/types').ScopedFetch;
scopedFs?: import('@ethosagent/types').ScopedFs;
scopedProcess?: import('@ethosagent/types').ScopedProcess;
attachments?: import('@ethosagent/types').ScopedAttachments;
dryRun?: boolean;
}

Members

FieldTypeDescription
sessionIdstringStable id of the current session.
sessionKeystringHuman-meaningful session key (e.g. cli:my-repo).
platformstringSurface the turn is running on (cli, telegram, discord, ...).
workingDirstringProcess cwd at turn start. Anchor relative paths against this.
agentIdstring | undefinedStable agent identity (multi-agent / mesh deployments).
personalityIdstring | undefinedActive personality. Thread through to memory and storage.
memoryScope'global' | 'per-personality' | undefinedResolved memory scope for this turn.
memoryScopeIdstring | undefinedOpaque scope id resolved by AgentLoop. When present, memory tools use it directly instead of deriving personality:<id> from personalityId and memoryScope.
teamIdstring | undefinedActive team id. Set by AgentLoop when the loop runs inside a team (WiringConfig.teamName). Team memory tools use this to build the team:<id> scope id. Absent when running solo.
currentTurnnumber1-indexed turn counter for the session.
messageCountnumberTotal messages in the session so far.
abortSignalAbortSignalFires when the user cancels or the turn times out. Wire into fetch, child processes, anywhere blocking.
emit(ev: ToolProgressEvent) => voidEmits a tool_progress event. See ToolProgressEvent.
resultBudgetCharsnumberMaximum characters the success value may contain before truncation. See tool-result-budget.
storageStorage | undefinedPer-turn Storage decorated with the personality's fs_reach allowlist. Tools that touch ~/.ethos/ must use this rather than node:fs.
readMtimesMap<string, { mtimeMs: number; readAtTurn: number }> | undefinedPer-run mtime registry for stale-write prevention. Populated by read_file; checked by write_file / patch_file before writing. Absent in tests that do not wire AgentLoop.
networkPolicyobject | undefinedPer-personality network reach. URL-capable tools must thread this through safeFetch from @ethosagent/safety-network.
kvStoreKeyValueStore | undefinedKey-value storage capability. See tool-capabilities.
secretsResolverScopedSecretsResolver | undefinedSecrets resolution capability. See tool-capabilities.
scopedFetchScopedFetch | undefinedScoped HTTP fetch capability. See tool-capabilities.
scopedFsScopedFs | undefinedScoped filesystem capability. See tool-capabilities.
scopedProcessScopedProcess | undefinedScoped process execution capability. See tool-capabilities.
attachmentsScopedAttachments | undefinedAttachment handling capability. See tool-capabilities.
dryRunboolean | undefinedWhen true, the tool should return synthetic results without performing side effects.

Notes

  • emit defaults events to audience: 'internal' if the tool omits the field. Opt into 'user' only when silent latency would confuse the user.
  • storage is undefined in some test wirings -- tools that need filesystem access should fall back gracefully (e.g. read-only) rather than crash.
  • The same abortSignal is passed to the LLM call. Once aborted, expect abortSignal.aborted === true for the rest of the turn.
  • readMtimes enables the STALE_WRITE error code. Tools skip the mtime check when the map is undefined.

Tool<TArgs>

Signature

import type { Tool, ToolContext, ToolResult } from '@ethosagent/types';

export interface Tool<TArgs = unknown> {
name: string;
description: string;
schema: Record<string, unknown>;
toolset?: string;
maxResultChars?: number;
capabilities: import('@ethosagent/types').ToolCapabilities;
execute: (args: TArgs, ctx: ToolContext) => Promise<ToolResult>;
isAvailable?: () => boolean;
alwaysInclude?: boolean;
outputIsUntrusted?: boolean;
}

Members

FieldTypeDescription
namestringUnique identifier exposed to the LLM. Conventionally snake_case (read_file, web_search).
descriptionstringOne-paragraph natural-language description the LLM reads to decide when to call this tool.
schemaRecord<string, unknown>JSON Schema for the args object. The LLM sees this and constructs calls against it.
toolsetstring | undefinedGroup label (file, web, terminal, ...). Used by ToolRegistry.getForToolset and personality toolset filtering.
maxResultCharsnumber | undefinedPer-call output cap. Combined with the turn-wide budget: Math.min(perCallBudget, maxResultChars ?? perCallBudget). See tool-result-budget.
capabilitiesToolCapabilitiesDeclares which scoped capabilities the tool requires (fs, network, process, secrets, kv). See tool-capabilities.
execute(args: TArgs, ctx: ToolContext) => Promise<ToolResult>Body. Must return a ToolResult; thrown errors become code: 'execution_failed'.
isAvailable() => boolean | undefinedOptional gate. Called every time the tool list is built -- return false to hide when a dependency (API key, binary) is missing.
alwaysIncludeboolean | undefinedWhen true, the tool ignores personality.toolset filtering. Reserve for framework-internal tools (e.g. get_skill).
outputIsUntrustedboolean | undefinedWhen true, AgentLoop sanitises chat-template tokens in the success output and wraps it in <untrusted source="..." tool="...">...</untrusted>. Set on every tool that returns adversary-controlled content (file contents, web pages, email bodies, subprocess stdout).

Notes

  • TArgs is the runtime type of the parsed args. The framework does not validate against schema -- pair the type with a Zod / Valibot parser inside execute if you need strict checking.
  • A tool whose name starts with mcp__<server>__ is treated as an MCP-server tool and gated by personality.mcp_servers rather than personality.toolset.

ToolFilterOpts

Signature

import type { ToolFilterOpts } from '@ethosagent/types';

export interface ToolFilterOpts {
allowedMcpServers?: string[];
allowedPlugins?: string[];
}

Members

FieldTypeDescription
allowedMcpServersstring[] | undefinedMCP server allowlist. Tools named mcp__<server>__* are excluded unless their server name is in this list. undefined means no MCP filter.
allowedPluginsstring[] | undefinedPlugin allowlist. Tools registered by a plugin are excluded unless their pluginId is in this list. undefined allows all plugin tools. [] allows only built-in (non-plugin) tools.

ToolReducerContext

Signature

import type { ToolReducerContext } from '@ethosagent/types';

export interface ToolReducerContext {
args: unknown;
turnCount: number;
}

Members

FieldTypeDescription
argsunknownThe original args passed to the tool's execute call. Useful for reducers that need to know what was requested (e.g. a read_file reducer that strips differently based on the requested line range).
turnCountnumberCurrent turn count. Reducers may apply more aggressive trimming on later turns when context pressure is higher.

ToolResultReducer

Signature

import type { ToolResultReducer, ToolResult, ToolReducerContext } from '@ethosagent/types';

export interface ToolResultReducer {
readonly toolName: string;
reduce(result: ToolResult, ctx: ToolReducerContext): ToolResult;
}

Members

FieldTypeDescription
toolNamereadonly stringName of the tool this reducer applies to. Exact match -- no regex, no wildcards.
reduce(result: ToolResult, ctx: ToolReducerContext) => ToolResultTransform a tool result into a signal-only form. Must be deterministic: same input must produce same output. No LLM calls. Must not throw -- return the original result on any internal error.

Notes

  • Reducers run inside ToolRegistry.executeParallel after execute returns and before the result is placed into the LLM context.
  • A reducer that throws violates the contract. Defensive callers wrap the call, but the reducer itself must handle its own errors.
  • Reducers must not call LLM APIs. They are a deterministic, synchronous-shaped transform (the signature is sync despite operating on ToolResult).

ToolResultReducerRegistry

Signature

import type { ToolResultReducerRegistry } from '@ethosagent/types';

export interface ToolResultReducerRegistry {
register(reducer: ToolResultReducer): () => void;
get(toolName: string): ToolResultReducer | undefined;
}

Members

MethodReturnsDescription
register() => voidRegister a reducer for a specific tool name. Returns a cleanup function that unregisters the reducer. Throws if a reducer for the same toolName is already registered -- one reducer per tool.
getToolResultReducer | undefinedLook up the reducer for a tool by name. Returns undefined if no reducer is registered.

Notes

  • Duplicate registration throws -- this is intentional. Two reducers for the same tool would produce ambiguous output. If a plugin needs to override a built-in reducer, unregister the existing one first (via the cleanup function) then register the replacement.
  • The cleanup function returned by register is idempotent -- calling it twice is safe.

Used by

ConsumerRole
extensions/tools-file/src/read_file, write_file, patch_file, search_files.
extensions/tools-terminal/src/terminal (bash subprocess).
extensions/tools-web/src/web_search, fetch_url.
extensions/tools-browser/src/Playwright-driven browser_* tools.
extensions/tools-code/src/lint, typecheck, run_tests.
extensions/tools-memory/src/memory_read, memory_write, session_search.
extensions/tools-todo/src/TODO list CRUD.
extensions/tools-mcp/src/Bridges MCP-server tools into the registry.
extensions/tools-delegation/src/task -- spawns subagents.
packages/core/src/tool-registry.tsDefaultToolRegistry.executeParallel invokes execute for every Tool and applies ToolResultReducer to results.
packages/plugin-sdk/src/tool-helpers.tsdefineTool<TArgs> factory + ok / err ToolResult shorthands.

See also