5ff277349c
- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt. - Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`. - Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present. - Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type. - Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`. - Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins. - Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
208 lines
7.5 KiB
Markdown
208 lines
7.5 KiB
Markdown
# 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)
|
|
|
|
- **Custom tool**: callable by the model during a turn (`execute` + Zod parameter 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
|
|
|
|
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.
|
|
|
|
```text
|
|
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 (`~/.omp/agent/tools`, `.omp/tools`)
|
|
- Claude config (`~/.claude/tools`, `.claude/tools`)
|
|
- Codex config (`~/.codex/tools`, `.codex/tools`)
|
|
- Claude marketplace plugin cache provider
|
|
2. Installed plugin manifests (`~/.omp/plugins/node_modules/*` via plugin loader)
|
|
3. Explicit configured paths passed to the loader
|
|
|
|
### Important behavior
|
|
|
|
- Duplicate resolved paths are deduplicated.
|
|
- Tool name conflicts are rejected against built-ins and already-loaded custom tools.
|
|
- `.md` and `.json` files 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
|
|
|
|
A custom tool module must export a function (default export preferred):
|
|
|
|
```ts
|
|
import type { CustomToolFactory } from "@oh-my-pi/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().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;
|
|
```
|
|
|
|
Schemas are authored with Zod (`pi.zod`) and flow through the shared validation/wire pipeline.
|
|
|
|
Factory return type:
|
|
|
|
- `CustomTool`
|
|
- `CustomTool[]`
|
|
- `Promise<CustomTool | CustomTool[]>`
|
|
|
|
## API surface passed to factories (`CustomToolAPI`)
|
|
|
|
From `types.ts` and `loader.ts`:
|
|
|
|
- `cwd`: host working directory
|
|
- `exec(command, args, options?)`: process execution helper
|
|
- `ui`: UI context (can be no-op in headless modes)
|
|
- `hasUI`: `false` in non-interactive flows
|
|
- `logger`: shared file logger
|
|
- `typebox`: zod-backed compatibility shim for legacy TypeBox-style schemas
|
|
- `zod`: injected `zod/v4` module (canonical for new schemas)
|
|
- `pi`: injected `@oh-my-pi/pi-coding-agent` exports
|
|
- `pushPendingAction(action)`: register a preview action finalized via plain-text writes to `/xdev/resolve` or `/xdev/reject` (`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
|
|
|
|
`CustomTool.execute` signature:
|
|
|
|
```ts
|
|
execute(toolCallId, params, onUpdate, ctx, signal);
|
|
```
|
|
|
|
- `params` is statically typed from your Zod/TypeBox schema via `Static<TParams>`.
|
|
- Runtime argument validation happens before execution in the agent loop.
|
|
- `onUpdate` emits partial results for UI streaming.
|
|
- `ctx` includes `sessionManager`, `modelRegistry`, current `model`, `isIdle()`, `hasQueuedMessages()`, `abort()`, and optional `settings`, `fetch`, and `autoApprove`.
|
|
- `signal` carries cancellation.
|
|
|
|
`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
|
|
|
|
- Tools are wrapped into `AgentTool` instances (`CustomToolAdapter` or 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 `--tools` currently validates only built-in tool names; custom tool inclusion is handled through discovery/registration paths and SDK options.
|
|
|
|
## Rendering hooks
|
|
|
|
Optional rendering hooks:
|
|
|
|
- `renderCall(args, options, theme)`
|
|
- `renderResult(result, options, theme, args?)`
|
|
|
|
Runtime behavior in TUI:
|
|
|
|
- If hooks exist, tool output is rendered inside a `Box` container.
|
|
- `renderResult` receives `{ expanded, isPartial, spinnerFrame? }`.
|
|
- Renderer errors are caught and logged; UI falls back to default text rendering.
|
|
|
|
## Session/state handling
|
|
|
|
Optional `onSession(event, ctx)` receives session lifecycle events, including:
|
|
|
|
- `start`, `switch`, `branch`, `tree`, `shutdown`
|
|
- `auto_compaction_start`, `auto_compaction_end`
|
|
- `auto_retry_start`, `auto_retry_end`
|
|
- `ttsr_triggered`, `todo_reminder`
|
|
|
|
Use `ctx.sessionManager` to reconstruct state from history when branch/session context changes.
|
|
|
|
## Failures and cancellation semantics
|
|
|
|
### Synchronous/async failures
|
|
|
|
- Throwing (or rejected promises) in `execute` is treated as tool failure.
|
|
- Agent runtime converts failures into tool result messages with `isError: true` and error text content.
|
|
- With extension wrappers, `tool_result` handlers can further rewrite content/details and even override error status.
|
|
|
|
### Cancellation
|
|
|
|
- Agent abort propagates through `AbortSignal` to `execute`.
|
|
- Forward `signal` to subprocess work (`pi.exec(..., { signal })`) for cooperative cancellation.
|
|
- `ctx.abort()` lets a tool request abort of the current agent operation.
|
|
|
|
### onSession errors
|
|
|
|
- `onSession` errors are caught and logged as warnings; they do not crash the session.
|
|
|
|
## Real constraints to design for
|
|
|
|
- Tool names must be globally unique in the active registry.
|
|
- Prefer deterministic, schema-shaped outputs in `details` for renderer/state reconstruction.
|
|
- Guard UI usage with `pi.hasUI`.
|
|
- Treat `.md`/`.json` in tool directories as metadata, not executable modules.
|