- Home
- Documentation
- runtime-tools
- Custom Tools
Custom Tools
Custom Tools
Section titled “Custom Tools”Custom tools are model-callable functions that plug into the same tool execution pipeline as built-in tools.
A custom tool is a TypeScript/JavaScript module that exports a factory. The factory receives a host API (CustomToolAPI) and returns one tool or an array of tools.
What this is (and is not)
Section titled “What this is (and is not)”- Custom tool: callable by the model during a turn (
execute+ TypeBox schema). - Extension: lifecycle/event framework that can register tools and intercept/modify events.
- Hook: external pre/post command scripts.
- Skill: static guidance/context package, not executable tool code.
If you need the model to call code directly, use a custom tool.
Integration paths in current code
Section titled “Integration paths in current code”There are two active integration styles:
-
SDK-provided custom tools (
options.customTools)- Wrapped into agent tools via
CustomToolAdapteror extension wrappers. - Always included in the initial active tool set in SDK bootstrap.
- Wrapped into agent tools via
-
Filesystem-discovered modules via loader API (
discoverAndLoadCustomTools/loadCustomTools)- Exposed as library APIs in
src/extensibility/custom-tools/loader.ts. - Host code can call these to discover and load tool modules from config/provider/plugin paths.
- Exposed as library APIs in
Model tool call flow
LLM tool call │ ▼Tool registry (built-ins + custom tool adapters) │ ▼CustomTool.execute(toolCallId, params, onUpdate, ctx, signal) │ ├─ onUpdate(...) -> streamed partial result └─ return result -> final tool content/detailsDiscovery locations (loader API)
Section titled “Discovery locations (loader API)”discoverAndLoadCustomTools(configuredPaths, cwd, builtInToolNames) merges:
- Capability providers (
toolCapability), including:- Native OMP config (
~/.xcsh/agent/tools,.xcsh/tools) - Claude config (
~/.claude/tools,.claude/tools) - Codex config (
~/.codex/tools,.codex/tools) - Claude marketplace plugin cache provider
- Native OMP config (
- Installed plugin manifests (
~/.xcsh/plugins/node_modules/*via plugin loader) - Explicit configured paths passed to the loader
Important behavior
Section titled “Important behavior”- Duplicate resolved paths are deduplicated.
- Tool name conflicts are rejected against built-ins and already-loaded custom tools.
.mdand.jsonfiles are discovered as tool metadata by some providers, but the executable module loader rejects them as runnable tools.- Relative configured paths are resolved from
cwd;~is expanded.
Module contract
Section titled “Module contract”A custom tool module must export a function (default export preferred):
import type { CustomToolFactory } from "@f5xc-salesdemos/xcsh";
const factory: CustomToolFactory = (pi) => ({ name: "repo_stats", label: "Repo Stats", description: "Counts tracked TypeScript files", parameters: pi.typebox.Type.Object({ glob: pi.typebox.Type.Optional(pi.typebox.Type.String({ default: "**/*.ts" })), }),
async execute(toolCallId, params, onUpdate, ctx, signal) { onUpdate?.({ content: [{ type: "text", text: "Scanning files..." }], details: { phase: "scan" }, });
const result = await pi.exec("git", ["ls-files", params.glob ?? "**/*.ts"], { signal, cwd: pi.cwd }); if (result.killed) { throw new Error("Scan was cancelled"); } if (result.code !== 0) { throw new Error(result.stderr || "git ls-files failed"); }
const files = result.stdout.split("\n").filter(Boolean); return { content: [{ type: "text", text: `Found ${files.length} files` }], details: { count: files.length, sample: files.slice(0, 10) }, }; },
onSession(event) { if (event.reason === "shutdown") { // cleanup resources if needed } },});
export default factory;Factory return type:
CustomToolCustomTool[]Promise<CustomTool | CustomTool[]>
API surface passed to factories (CustomToolAPI)
Section titled “API surface passed to factories (CustomToolAPI)”From types.ts and loader.ts:
cwd: host working directoryexec(command, args, options?): process execution helperui: UI context (can be no-op in headless modes)hasUI:falsein non-interactive flowslogger: shared file loggertypebox: injected@sinclair/typeboxpi: injected@f5xc-salesdemos/xcshexportspushPendingAction(action): register a preview action for hiddenresolvetool (docs/resolve-tool-runtime.md)
Loader starts with a no-op UI context and requires host code to call setUIContext(...) when real UI is ready.
Execution contract and typing
Section titled “Execution contract and typing”CustomTool.execute signature:
execute(toolCallId, params, onUpdate, ctx, signal)paramsis statically typed from your TypeBox schema viaStatic<TParams>.- Runtime argument validation happens before execution in the agent loop.
onUpdateemits partial results for UI streaming.ctxincludes session/model state and anabort()helper.signalcarries cancellation.
CustomToolAdapter bridges this to the agent tool interface and forwards calls in the correct argument order.
How tools are exposed to the model
Section titled “How tools are exposed to the model”- Tools are wrapped into
AgentToolinstances (CustomToolAdapteror extension wrappers). - They are inserted into the session tool registry by name.
- In SDK bootstrap, custom and extension-registered tools are force-included in the initial active set.
- CLI
--toolscurrently validates only built-in tool names; custom tool inclusion is handled through discovery/registration paths and SDK options.
Rendering hooks
Section titled “Rendering hooks”Optional rendering hooks:
renderCall(args, theme)renderResult(result, options, theme, args?)
Runtime behavior in TUI:
- If hooks exist, tool output is rendered inside a
Boxcontainer. renderResultreceives{ expanded, isPartial, spinnerFrame? }.- Renderer errors are caught and logged; UI falls back to default text rendering.
Session/state handling
Section titled “Session/state handling”Optional onSession(event, ctx) receives session lifecycle events, including:
start,switch,branch,tree,shutdownauto_compaction_start,auto_compaction_endauto_retry_start,auto_retry_endttsr_triggered,todo_reminder
Use ctx.sessionManager to reconstruct state from history when branch/session context changes.
Failures and cancellation semantics
Section titled “Failures and cancellation semantics”Synchronous/async failures
Section titled “Synchronous/async failures”- Throwing (or rejected promises) in
executeis treated as tool failure. - Agent runtime converts failures into tool result messages with
isError: trueand error text content. - With extension wrappers,
tool_resulthandlers can further rewrite content/details and even override error status.
Cancellation
Section titled “Cancellation”- Agent abort propagates through
AbortSignaltoexecute. - Forward
signalto subprocess work (pi.exec(..., { signal })) for cooperative cancellation. ctx.abort()lets a tool request abort of the current agent operation.
onSession errors
Section titled “onSession errors”onSessionerrors are caught and logged as warnings; they do not crash the session.
Real constraints to design for
Section titled “Real constraints to design for”- Tool names must be globally unique in the active registry.
- Prefer deterministic, schema-shaped outputs in
detailsfor renderer/state reconstruction. - Guard UI usage with
pi.hasUI. - Treat
.md/.jsonin tool directories as metadata, not executable modules.