Skip to main content
reference

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;
rootSessionKey?: string;
jobId?: string;
origin?: string;
a2aDelegation?: { traceId: string; depth: number; reserveOutbound: () => boolean };
scriptTools?: import('@ethosagent/types').ScriptToolsApi;
toolCallId?: string;
personalityId?: string;
memoryScopeId?: string;
userScopeId?: 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;
getContext?: <T>(key: string) => T | undefined;
setContext?: <T>(key: string, value: T) => void;
llm?: import('@ethosagent/types').SimpleCompletion;
}

Field order and types match packages/types/src/tool.ts. Doc comments are omitted, and the source's relative import('./storage')-style paths are written as the @ethosagent/types re-exports.

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).
rootSessionKeystring | undefinedRoot session key for background-job scoping. Set by the background executor on a detached child so nested spawns inherit the same root. Absent on ordinary turns; background tools fall back to sessionKey.
jobIdstring | undefinedThe background job id, stamped by BackgroundExecutor.runOne. Always undefined on a foreground turn — there is no fallback. clarify keys its question lane on jobId ?? sessionId.
originstring | undefinedWhere the turn came from, as platform:chatId, on channel turns. Unset otherwise. goal_create stamps it onto Goal.origin.
a2aDelegationobject | undefinedSet by the A2A runner when the turn services an inbound A2A task. The outbound A2A tool signs an onward call at depth + 1 and calls reserveOutbound() against the per-trace fan-out budget. Absent on normal turns.
scriptToolsScriptToolsApi | undefinedSet per turn when a ScriptToolBridge is wired. run_code threads it so an in-script ethos.call(name, args) goes through the same enforcement as a model-issued call. Holds live callbacks, so it never travels in the serializable ToolExecuteRequest.
toolCallIdstring | undefinedThis call's own id, copied from ToolExecuteRequest.toolCallId. run_code passes it as parentToolCallId to namespace inner calls. Hand-built contexts may omit it; tools must tolerate absence.
personalityIdstring | undefinedActive personality. Thread through to memory and storage.
memoryScopeIdstring | undefinedThe turn's memory scope, always personality:<personalityId> (memScopeId in packages/core/src/agent-loop/stages/turn-setup.ts). Memory tools pass it to the provider unchanged. AgentLoop sets it on every call, and the realtime voice host sets the same scope for a call with a personality (createRealtimeToolHost in extensions/tools-voice/src/realtime-host.ts). A context built outside any personality's conversation — the web tool-test probe, a plugin panel call — has none, and memory_read / memory_write / meet_join then return not_available rather than invent a scope (NO_MEMORY_SCOPE in extensions/tools-memory/src/index.ts).
userScopeIdstring | undefineduser:<userId> when the turn carries a user id — a gateway turn whose sender the identity map resolved. Memory tools use it for store: 'user'. Absent under ethos chat, where store: 'user' reads the personality's own USER.md.
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.
getContext(<T>(key: string) => T | undefined) | undefinedReads a value from the run's shared context store (ContextStore, packages/core/src/context-store.ts). Absent where the context belongs to no run, such as the web tool-test probe.
setContext(<T>(key: string, value: T) => void) | undefinedWrites a value into the same store, visible to later tool calls in the run. Absent under the same conditions as getContext.
llmSimpleCompletion | undefinedA one-shot completion client (packages/types/src/plugin-llm.ts). AgentLoop sets it on the turn's effective model (SimpleCompletionImpl in packages/core/src/agent-loop/stages/tool-processing.ts). Absent on contexts built outside a turn; a tool must tolerate that.

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​