MusePi

Custom Tools

English | 中文

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)

If you need the model to call code directly, use a custom tool.

Integration paths in current code

There are two active integration styles:

  1. SDK-provided custom tools (options.customTools)
    • Wrapped into agent tools via CustomToolAdapter or extension wrappers.
    • Always included in the initial active tool set in SDK bootstrap.
  2. 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.
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/details

Discovery locations (loader API)

discoverAndLoadCustomTools(configuredPaths, cwd, builtInToolNames) merges:

  1. Capability providers (toolCapability), including:
    • Native OMP config (~/.musepi/agent/tools, .musepi/tools)
    • Claude config (~/.claude/tools, .claude/tools)
    • Codex config (~/.codex/tools, .codex/tools)
    • Claude marketplace plugin cache provider
  2. Installed plugin manifests (~/.musepi/plugins/node_modules/* via plugin loader)
  3. Explicit configured paths passed to the loader

Important behavior

Module contract

A custom tool module must export a function (default export preferred):

import type { CustomToolFactory } from "@musepi/pi-coding-agent";

const factory: CustomToolFactory = (pi) => ({
  name: "repo_stats",
  label: "Repo Stats",
  description: "Counts tracked TypeScript files",
  parameters: pi.zod.object({
    glob: pi.zod.string().optional(),
  }),

  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;

Parameter schemas may use the Zod-compatible omptype builder (pi.zod), native omptype builder (pi.arktype), or legacy-compatible TypeBox shim (pi.typebox) and flow through the shared validation/wire pipeline.

Factory return type:

API surface passed to factories (CustomToolAPI)

From types.ts and loader.ts:

Execution contract and typing

CustomTool.execute signature:

execute(toolCallId, params, onUpdate, ctx, signal);

CustomToolAdapter bridges this to the agent tool interface and forwards calls in the correct argument order.

Tool definitions may also declare strict, hidden, deferrable, mcpServerName, mcpToolName, approval, and formatApprovalDetails.

How tools are exposed to the model

Rendering hooks

Optional rendering hooks:

Runtime behavior in TUI:

Session/state handling

Optional onSession(event, ctx) receives session lifecycle events, including:

Use ctx.sessionManager to reconstruct state from history when branch/session context changes.

Failures and cancellation semantics

Synchronous/async failures

Cancellation

onSession errors

Real constraints to design for