Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 20. októbra 2026

Prechádzať dokumentáciu · Tool registration and schemas

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Developer reference

Tool registration and schemas

Entry point and tool schemas

The entry exports a synchronous or asynchronous register(ctx):

import { defineTool } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

const result = (text) => ({
  content: [{ type: "text", text }],
  details: {}
});

export function register(ctx) {
  ctx.registerTool(defineTool({
    name: "MyTool",
    label: "Example tool",
    description: "Returns the supplied text.",
    parameters: Type.Object({
      value: Type.String({ description: "Text to return." })
    }),
    execute: async (_callId, params) => {
      if (typeof params.value !== "string") {
        return result("value must be a string");
      }
      return result(params.value);
    }
  }));

  ctx.logger.info("example tool registered");
}

Use PI's defineTool and TypeBox schemas. The schema is the tool's input contract exposed to the model. Use the TypeBox constructors for objects, strings, numbers, booleans, arrays, unions, optionals, and literals. Validate data from external systems again inside the handler. A normal result has a content array containing text or other PI-supported content and a details object for host-only metadata.

File and directory parameter names

Use file_path for a tool's primary single-file target, whether it reads, writes, shares or executes a definition file. Read, Write, Edit, ShareFile, ShareImage and WorkflowStart use this name. Use path for a directory or search scope, including a scope that can select either a file or a directory; Glob, Grep, Search, ListDir, FileInfo and GitStatus use it. This follows the Claude Code Read, Edit and Glob schemas.

Keep distinct roles distinct when a tool needs two files: EditImage reads file_path and writes its optional output_path. An execution working directory remains cwd; an API route remains path. These fields are not single-file targets.

Renaming a parameter changes the live schema and every caller together, without aliases. Reject unknown parameters instead of silently using a default target. Journal arguments remain immutable. The existing common subject-key reader in src/brain/messageView.ts already handles both file targets and search scopes; attachment ownership and retention read result references, never source paths. Do not add a historical argument reader or rewrite journal entries.

A failure the model cannot fix by calling differently is thrown rather than returned as text: the host turns a throw into a tool result flagged is_error. That is every transport and host fault (a network, DNS or socket error, an origin answering 5xx, a dead remote transport), never a bad argument, a missing resource or a policy refusal, which stay text results the model can act on. Use markTransportFailure(error) from elowen-plugin-shared/transport to tag such a failure and isTransportFailure in the tool's own catch-all so it is rethrown instead of flattened into an answer.

registerTool(tool, { external: true }) marks an externally authored schema. The host validates this boolean and normalizes an omitted value to false for all registrations. PluginToolMetadata is the normalized host-side type; tool names never imply origin, and an explicit false stays internal even with an mcp__ prefix. The bundled MCP bridge supplies external: true, including its snapshot-backed runner registrations. Metadata follows the accepted tool owner through registry merges; a rejected collision contributes neither origin nor read-only classification.

During session composition, external descriptions are capped at 1,000 UTF-8 bytes and definitions above 8,000 serialized bytes receive a permissive object parameter schema with an omission notice. The external owner remains the argument validator. External schemas are never reconstructed to add reason; internal tools receive the optional reason status field, except the fixed Bash and ToolSearch exclusions. Advertise one flat object without root combinators: OpenAI-compatible Responses endpoints can reject those schemas before the turn runs. ShareImage checks its file-or-latest choice in its handler. Its flat schema lets an unknown-only call reach that check, which names file_path and latest in the refusal. Origin classification is independent of deferral. Cache diagnostics receive a separate projection of external names and redact both individual names and hosted namespaces containing external tools. This metadata is never inserted into provider payloads. Work is linear in the tool catalog plus the external schemas inspected at composition.

A declared provides.readOnlyTools: ['mcp__*'] expands only to that plugin's registered external tools and requires the matching provides.tools entry. Exact read-only names retain their own declaration and ownership checks. Neither form grants tool execution access or changes account wildcard grammar, output artifact selection, automatic image previews or locked deferral. Plan mode admits exact read-only names but never an external tool, whichever form declared it. Without external registrations, local schemas and diagnostic names stay local.

registerTool(tool, { ownerUserId, projectId, platform }) can scope a contribution to one account, one Project, or conversations from a named platform. Without these options the contribution is available instance-wide. A platform-scoped tool is omitted from other sessions' model tool lists and catalog, not merely rejected when called. This is useful for tools that require a visitor's page or another platform-specific turn. Platform scoping does not replace execution-time authorization. Resolve identity and access at execution time:

const identity = ctx.currentIdentity();
const access = ctx.currentAccess();
const accountUserId = ctx.currentAccountUserId();

Do not capture those values during registration or reuse them across turns.

PluginAccessDescriptor.planFile?: string is a core-minted exact guest path, present only on a managed or local Project conversation's own Plan turn. Sandbox consumes it at each file operation to admit that single plan write/upload and its create-only parent directories; the paired-computer executor admits only a write to that same exact path and narrows the operation to its parent directory. It grants no remove, rename, copy, lifecycle or Desktop permission. It is absent on host and Build turns and on all delegated read-only origins. Do not construct or forward this allowance to a child; read currentAccess() at execution time. Command execution follows the core non-destructive permission boundary, including read-only children, and does not require a writable environment descriptor. currentAccess().readOnly carries both an admitted Plan restriction and the immutable delegated read-only origin. An imposed delegated origin also keeps planMode true for further delegation even though the child itself starts in Build and receives no owner-chat mode directive. Plugins must never clear either restriction when constructing child access.

currentAccess().admin and projectIds are the live effective policy scope, intersected with the session's captured ceiling. Account demotion and project revocation apply without respawning; later grants cannot widen a previously scoped session. File and directory tools consume the same host policy. currentIdentity() also removes captured admin/owner privilege when live host authority is revoked; it never promotes a turn. Read access immediately before each operation, including after an asynchronous wait. Queued host Write/Edit recheck the anchored path inside the mutation queue. currentAccess resolves account-backed role, Project scope and roots in one fresh authorization evaluation; identity/account projections reuse facts only within that descriptor. Files Read derives read-state identity from the same metadata returned to its caller, including PDF/notebook results. Host skill resources stay host-keyed, local Windows paths keep their native platform key, and Write/Edit retain their post-queue live checks and byte/version read-before-write guard.

Tool calls still pass through the host permission boundary, plan-mode policy, plugin grant checks, and applicable hooks.

Who may call a plugin tool is decided by the account's plugin grant and tool deny-list, not by the tool. A tool does not add an identity.owner check of its own to restrict access; the bundled Discord API and role tools, for example, rely on the grant. Use identity.owner for plugin state that only the instance operator may change, not for tool entitlement.

The manifest metadata completes the runtime tool definition:

  • Successful output is hidden unless showOutput matches the tool.
  • Plan mode keeps every tool advertised and executes only the read-only set: the core read-only tools plus every plugin's exact provides.readOnlyTools names, and the host plan defaults (CORE_TOOL_DEFAULTS in src/brain/toolMetadata.ts). A tool not declared there is refused at execution time. The same declaration decides what read-only sub-agents receive, so declare a tool only when it changes nothing outside the agent's own working state.
  • deferLoading defers matching tools even in a small session, unless deferral is switched off or an administrator overrides the tool. This is useful for optional or large tool families. A read-only declaration does not keep a tool loaded: plan mode reaches deferred read-only tools through the loader like any other turn.
  • A tool name in provides.tools is an audit declaration. The actual registration remains the source of the live contribution report.

Owning code and consumers

  • The PluginContext methods and the registration option types live in src/plugins/api.ts: PluginToolRegistrationOptions at line 2503 (ownerUserId, projectId, platform, external), registerTool at line 2562, currentAccess at line 2881, currentIdentity at line 2891, currentAccountUserId at line 2905, and logger at line 3152. PluginAccessDescriptor is at line 2533; the turn-internal extension that adds planMode and principal is InternalAccessDescriptor in src/plugins/pathGuard.ts at line 47.
  • The manifest fields showOutput and deferLoading are declared in src/plugins/manifest.ts at lines 169 and 170. provides.readOnlyTools is at line 157 and its validation at lines 321 to 325. Plan-mode admission is planModeToolNames in src/brain/toolMetadata.ts at line 51, over CORE_TOOL_DEFAULTS at line 18.
  • The external caps (1,000 UTF-8 bytes for descriptions, 8,000 serialized bytes for definitions) are the constants in src/brain/toolSchemaCap.ts at lines 24 and 29.
  • markTransportFailure and isTransportFailure are in packages/plugin-shared/transport.mjs at lines 20 and 28, exported as elowen-plugin-shared/transport.
  • The reason status field is owned by src/brain/toolReason.ts: the exclusion set (ToolSearch, Bash and every external tool) is isReasonExcluded at line 33, and withReason at line 41 is applied in src/brain/session/capabilities.ts.
  • The bundled MCP bridge is the real consumer of external: true; see plugins/mcp/index.mjs at line 739.

Absent behavior, limits and cost

  • A tool that is never registered is absent from the model's tool list. Manifest lists in provides are audit declarations and do not register anything.
  • External schema and description limits are applied when the session is composed. The object-root rule for local schemas, and what happens to a refused schema, are in row 1 of the seam catalog.
  • A tool's schema and description are part of the provider request, so their size costs input tokens on every turn that offers the tool.