# Dynamic tools

Use this reference when asked to add, change, debug, or explain dynamic tools. Do not inspect every existing definition unless the task requires it. Do not enable a bundled example unless the user explicitly asks.

## How it works

A dynamic tool connects one JavaScript method inside Pi's `exec` tool to one local command-line program:

```text
tools.<filename>(input string)
  → TOML command definition
  → local process
  → string result
```

Definitions are top-level `*.toml` files under the Pi agent directory's `dynamic-tools/` folder (`~/.pi/agent/dynamic-tools/` by default, or `$PI_CODING_AGENT_DIR/dynamic-tools/` when configured) and `<launch-directory>/.pi/dynamic-tools/` for trusted project-only tools. Only the launch directory is checked; parent directories are not searched. Project-local definitions are ignored unless Pi trusts the project and override same-named global definitions when active. Companion scripts may live in subdirectories. Pi rediscovers definitions before every `exec`; already-running cells keep the definitions they started with.

The JavaScript cell is isolated V8 with no direct filesystem, network, or Node access. The delegated command is not sandboxed by code mode: it runs locally with the user's permissions, Pi's working directory, and inherited environment.

## Dynamic tool or Pi extension?

Consider a dynamic tool first when the capability is command-backed, tool-shaped, and useful only occasionally. Deferred tools add no tool-specific system-prompt or provider-schema cost. A stable promoted tool adds only its name and usage rather than a full provider-visible JSON schema.

Use a dynamic tool when:

- one string can carry the input directly or as JSON;
- a local process can own validation, dependencies, filesystem or network access, and output formatting;
- the capability should compose with other calls inside `exec`;
- the tool is infrequent enough to stay deferred, or stable enough to promote deliberately.

Build a Pi extension when the capability needs lifecycle events, custom TUI, Pi session state, provider interception, tool-call interception, runtime tool activation, or a directly exposed structured schema. Do not build an extension merely to wrap a command that fits the dynamic-tool boundary.

## Add or change a tool

1. Confirm the command or script that should perform the work. TOML is only the bridge; complex parsing and behavior belong in the CLI.
2. Create the definitions directory if it does not exist. Choose a descriptive JavaScript-compatible filename. Start with a letter, `_`, or `$`; use only letters, numbers, `_`, and `$` after that.
3. Create or update only the relevant TOML and companion files.
4. Keep the tool deferred unless the user explicitly wants it named in every system prompt.
5. Validate the command independently when practical.
6. The changed catalog is available on the next `exec`. Do not send slash commands as agent tool calls.

The bundled `port_info` definition shows the required invocation contract and optional discovery detail:

```toml
usage = 'await tools.port_info(port_number)'
description = "Returns normalized JSON with listeners, connections, owning processes, parent processes, and service or container attribution when available."
command = "./port-info/port-info.mjs"
```

A structured tool still receives one string. The bundled `spawn_agent` example encodes its input as JSON and validates it in the CLI:

```toml
usage = '''await tools.spawn_agent(JSON.stringify({ agent_type: "explorer" | "reviewer", message: string, cwd?: string }))'''
description = "Relative cwd resolves from Pi's working directory."
command = "./spawn-agent/spawn-agent.mjs"
input = "stdin"
```

The model calls it with:

```js
const result = await tools.spawn_agent(JSON.stringify({
  agent_type: "explorer",
  message: "Find the authentication entry points and cite the relevant files.",
  cwd: "../another-repo",
}));
text(result);
```

## TOML fields

`usage` and `command` are required. Unknown fields, invalid TOML, and missing contracts disable only that named tool. The tool remains visible in `exec` and throws its configuration error when called, while `exec`, `wait`, and valid tools remain available. An invalid project-local definition still suppresses a same-named global definition rather than silently changing behavior. Invalid filename identifiers and unreadable directories, which cannot be represented as named tools, are reported through Pi.

- `command`: executable name or path.
- `args`: fixed string arguments placed before the model-provided input. Defaults to `[]`.
- `input`: `"arg"` appends the input as the final argument; `"stdin"` writes it to standard input. Defaults to `"arg"`.
- `usage`: required exact JavaScript invocation contract, without Markdown formatting. For structured input, show the object passed to `JSON.stringify`.
- `description`: optional discovery help not already clear from the name and usage.
- `output`: optional discovery help about a reliable result contract. It does not control, validate, or transform command output. Omit it when the result is free-form.
- `defer_loading`: defaults to `true`. Set to `false` only when the user wants the tool named in the system prompt.
- `yield_time_ms`: non-negative integer controlling how long `exec` initially waits when the source directly invokes this tool. It overrides the model's `// @exec` value and is not exposed as a model-facing argument. If one cell directly invokes several configured tools, the largest value wins.

Command resolution:

- Bare names resolve through inherited `PATH`.
- Absolute paths run directly.
- Relative command paths resolve from the directory containing the TOML file.
- Relative entries inside `args` are not rewritten; resolve them inside the CLI or use paths appropriate to Pi's working directory.
- Relative or absolute `.js`, `.mjs`, and `.cjs` commands run with Pi's JavaScript runtime.
- Commands run directly without a shell. Shell expansion, pipes, aliases, and redirection do not apply unless the configured command explicitly launches a shell.

## Deferred and promoted tools

Deferred is the cache-safe default:

```toml
usage = 'await tools.port_info(port_number)'
command = "./port-info/port-info.mjs"
```

A deferred tool remains callable but its name and help do not enter the system prompt or provider tool schema. Its metadata stays local in `ALL_TOOLS` until requested. The outer `exec` and `wait` tools are always registered, even with an empty catalog, so adding, removing, or editing deferred definitions during a session does not change that stable provider contract.

Inspect only the metadata needed:

```js
text(ALL_TOOLS.find(({ name }) => name === "port_info"));
```

Promote a stable, frequently used tool only by explicit choice:

```toml
defer_loading = false
usage = 'await tools.port_info(port_number)'
command = "./port-info/port-info.mjs"
```

Promotion adds the tool name and `usage` to the system prompt. Changing the promoted set, names, or usage changes that prompt and can invalidate its cache. Descriptions and `output` help remain available through `ALL_TOOLS` rather than being copied into the system prompt.

## Execution and failures

Every dynamic method accepts one string and resolves to one string.

- A successful command returns stdout with trailing whitespace removed.
- If stdout is empty, stderr is returned instead.
- If both are empty, the result is `(no output)`.
- A non-zero exit is an error and includes stderr when available.
- Cancellation stops the delegated process; on supported Unix-like systems it kills the process group.
- Combined stdout and stderr are limited to 50 KiB. Exceeding the limit terminates the process and returns an error.

The `exec` cell can compose calls with normal JavaScript, including `Promise.all`, `store`/`load`, progress notifications, images, yielding, and resumable `wait` cells. Read the `exec` tool contract for those runtime details rather than duplicating it here.

For long-running commands, set the definition's private `yield_time_ms` near the expected runtime and prefer one long `wait` over repeated short waits. Long waits remain cancellable, and `notify()` progress remains visible.

## Bundled examples

Examples are documentation and working reference code. Installing the package does not register or enable them. Do not copy an example merely because you read this file.

### `herdr_agent`

`herdr_agent.toml` and `herdr-agent/herdr-agent.mjs` find and coordinate Pi agents in Herdr panels. The calling Pi session must run inside Herdr. The tool handles focused panel discovery, messaging, waits, reads, and answers; use the `herdr` skill from `more_skills` for advanced workspace and pane orchestration.

### `more_skills`

`more_skills.toml` and `more-skills/more-skills.mjs` list additional skill names or return one skill's Markdown body. When copied into a global definitions directory it reads the Pi agent directory's `more-skills/`; from a project-local definitions directory it reads `<launch-directory>/.pi/more-skills/`.

### `sites` and `sites_documentation`

Keep `sites.toml`, `sites_documentation.toml`, and the shared `sites/` directory together. These deferred tools provide a curated private-API bridge to the ChatGPT Sites beta and bounded local/backend documentation. They use Pi's configured OpenAI Codex OAuth, keep credentials and secret values out of output, and require explicit user intent for production effects such as deployment, access, environment, or domain changes.

### `spawn_agent`

The package directory containing this file also contains:

```text
examples/spawn_agent.toml
examples/spawn-agent/
  spawn-agent.mjs
  explorer.prompt.md
  reviewer.prompt.md
```

When the user explicitly asks to enable the example, copy `examples/spawn_agent.toml` into the chosen global or project-local definitions directory and copy `examples/spawn-agent/` beside it, preserving that layout. The TOML's relative command then resolves to `spawn-agent/spawn-agent.mjs`.

The example demonstrates a promoted tool with JSON input:

```js
const result = await tools.spawn_agent(JSON.stringify({
  agent_type: "explorer",
  message: "Find the authentication entry points and cite the relevant files.",
  cwd: "../another-repo",
}));
text(result);
```

Its contract is:

- `agent_type`: required; `"explorer"` or `"reviewer"`.
- `message`: required standalone instructions.
- `cwd`: optional; defaults to Pi's working directory and resolves relative to it.

`explorer` runs GPT-5.6 Terra with low reasoning and a discovery-only appended system prompt.

`reviewer` runs GPT-5.6 Luna with medium reasoning and the same rubric as the review extension. Before starting Pi, the example resolves the Git repository root and prepares a user message with the current ref, selected local base branch, merge base, working-tree status, appropriate diff commands, and the caller's instructions. The rubric is appended to Pi's normal system prompt; project context remains loaded.

Both roles disable child skills and prompt templates. Their appended prompts prohibit further subagent delegation, while project context files and Pi's built-in discovery tools remain available.

### `port_info`

The package also contains:

```text
examples/port_info.toml
examples/port-info/port-info.mjs
```

This example compresses a platform-specific system investigation into one argument:

```js
const result = await tools.port_info("3000");
text(result);
```

It returns normalized JSON containing TCP/UDP listeners, active connections, owning process details, parent-process information, and service or container attribution when available. It reports partial-inspection and permission failures in `diagnostics` instead of hiding them, and caps endpoint results at 25 with bounded process metadata.

- Linux reads `/proc/net` and process metadata directly without requiring `ss` or `lsof`.
- macOS and other Unix-like systems use `lsof` and `ps` when available.
- Windows uses `Get-NetTCPConnection`, `Get-NetUDPEndpoint`, process metadata, and service metadata through PowerShell.

Process command lines and working directories can contain sensitive information. Enable or invoke this example only when that system inspection is appropriate.

When the user explicitly asks to enable it, copy `examples/port_info.toml` into the definitions directory and copy `examples/port-info/` beside it, preserving the layout.

### `vent`

The package also contains:

```text
examples/vent.toml
examples/vent/vent.mjs
```

This tool validates JSON containing `thought` and optional `trigger`, then appends one timestamped entry to `VENT.md`. It creates the file with a short purpose heading when needed. Copy the TOML and companion directory into `dynamic-tools/`, preserving the layout, only when the user asks to enable it.

### `workflows_create`

The package also contains:

```text
examples/workflows_create.toml
examples/workflows-create/workflows-create.mjs
```

This tool validates `name`, `description`, and Markdown `body`, then atomically creates or updates `.pi/workflows/<slug>/SKILL.md`. Copy the TOML and companion directory into `dynamic-tools/`, preserving the layout, only when the user asks to enable it.

### `semantic_grep`

The package also contains:

```text
examples/semantic_grep.toml
examples/semantic-grep/semantic-grep.mjs
```

This promoted tool searches an existing semantic-grep SQLite index with bounded output. It requires `@howaboua/pi-semantic-grep` to remain installed: that extension owns global/project configuration and session-start indexing, while the dynamic tool owns query execution. Copy the TOML and companion directory into `dynamic-tools/`, preserving the layout, only when the user asks to enable it.
