Tools and rules

A plugin can give every agent (Codex, Claude Code, OpenCode, every ACP agent) tools it can call and rules it follows. The user chooses in Settings > Tools how the agents see the tools:

  • Code mode (the default): the agent sees one tool, execute, and writes a short JavaScript program that calls the plugin tools as tools.<plugin>.<tool>(input). It can chain calls, loop and filter results before they reach its context.
  • Separate tools: each plugin tool is a tool of its own.

A plugin author writes the same plugin for both.

Code mode

The session's instructions list every tool the program may call, with its description, its input schema and what it returns. The agent's execute takes { code }: the body of an async function (or one expression, such as an async arrow function, which is called with tools). For example:

const [a, b] = await Promise.all([tools.weather.forecast({ city: "Oslo" }), tools.weather.forecast({ city: "Rome" })]);
console.log("both answered");
return { oslo: a, rome: b };

A tool is tools.<plugin>.<tool>; names with other characters than letters, digits, _ and $ are also there with _ in their place (tools.my_db.search_rows for the plugin my-db's search-rows). A call resolves to the tool's structuredContent when the tool has an outputSchema, otherwise to its text; a result with images resolves to its MCP content list. A failed call rejects with an Error whose message is the tool's text.

The program runs in a process of its own under the OS sandbox, in a fresh JavaScript runtime with the language and nothing else: no files, network, programs, timers, modules or host API. Each call goes back to the host, which runs the tool in its plugin as a direct call; calls made together run together. The program may use 256 MiB of memory and run JavaScript for at most 10 seconds between two tool answers, and it is stopped after 15 minutes in all. A program that waits for a promise nothing will settle ends with an error at once. The agent reads what the program returned as JSON text (a string as it is), then what it wrote with console.* (64 KiB at most); a program that threw, or went over a limit, is a failed result with the reason.

Rules

Rules need no code. List Markdown files in the manifest:

{ "name": "house-style", "version": "0.1.0", "contributes": { "rules": ["rules.md"] } }

Every new or resumed session of every agent gets the text of every enabled plugin's rules, under a heading with the plugin's name. The user can turn one plugin's rules off in Settings > Tools. How each agent receives them: Codex as developer instructions, Claude Code as an addition to its system prompt, OpenCode as the system text of each prompt, and ACP agents (which have no system prompt) as a block before the first message of a new session.

Tools

A plugin registers tools with api.tools.register, and needs tools.provide for it:

// plugins/weather/main.js
export function activate(api) {
  api.tools.register({
    name: "weather",
    description: "Current weather for a city.",
    inputSchema: { type: "object", properties: { city: { type: "string", description: "City name" } }, required: ["city"] },
    async run({ city }, ctx) {
      const response = await api.net.fetch(`https://wttr.in/${encodeURIComponent(city)}?format=3`, { signal: ctx.signal });
      return response.text();
    },
  });
}
{
  "name": "weather", "version": "0.1.0", "main": "main.js",
  "permissions": {
    "tools.provide": { "reason": "Give agents the weather tool" },
    "net": { "hosts": ["wttr.in"], "reason": "Ask wttr.in for the weather" }
  }
}

examples/hello-tools is a complete plugin with two tools and a rule.

Tools with Zod (TypeScript)

A plugin written with convergence/effect registers a tool with Zod schemas through the Tools service instead of writing JSON Schema by hand; the adapter derives the JSON Schema for the agent and parses the input before run sees it:

import * as Effect from "effect/Effect";
import * as z from "zod";
import { Tools, runPlugin } from "convergence/effect";

const program = Effect.gen(function* () {
  const tools = yield* Tools;
  yield* tools.register({
    name: "word-count",
    description: "Counts the words of a text.",
    input: z.object({ text: z.string() }),
    output: z.object({ words: z.number() }),
    run: ({ text }) => Effect.succeed({ words: text.split(/\s+/).filter(Boolean).length }),
  });
});

export const activate = runPlugin(program);

input and output are Zod schemas (z.toJSONSchema makes the JSON Schema); the result is parsed against output when there is one. Everything else (the fields of a tool, ctx and subagents) is the same as api.tools.register. The permission is the same tools.provide.

A tool

Field Meaning
name 1 to 64 letters, digits, _ or -. If two plugins use the same name, separate tools are named <plugin>_<name>.
description What the tool does and when to use it. The agent reads this.
inputSchema JSON Schema of the input, an object schema. Without one the tool takes an empty object.
outputSchema Optional JSON Schema of what run returns. In code mode a tool with one gives the program the parsed object; without one the program gets the text.
title, annotations Optional MCP fields: a display title, and the hints readOnlyHint, destructiveHint, idempotentHint, openWorldHint.
scope "plugins-workspace" offers the tool only to chats in the Plugins workspace (tools for working on plugins; the marketplace tools of the official market plugin are such tools, see ../AGENTS.md, Marketplace).
run(input, ctx) Does the work. May be async.

register returns { remove() }; the host learns of every change and asks for the list again.

run returns a string (text), any other value (sent as JSON text and as structuredContent), or a whole MCP result { content: [...], isError? } (content items { type: "text", text } or { type: "image", data, mimeType }). A thrown error becomes a failed result the agent reads; it never ends the turn.

ctx says who called:

Field Meaning
workspace absolute folder the session works in
workspaceId the workspace, for host calls
agentId for example codex, claude:work, acp:pi-acp
chatId the chat, when it is known (OpenCode shares one tool connection per folder, so its calls name the chat only while it is the one running there)
callId the agent's id for this call, when it gave one
signal an AbortSignal that aborts when the call is given up (the turn was stopped); pass it on to api.net.fetch
subagent start(options): shows a subagent under this call (see Subagents)
scope the host's id for this call's subagents while it runs in a chat; absent outside a chat

console.log goes to the app's log. A tool runs in its plugin's runtime, under its limits (ui-plugins.md, Limits): long work waits on the host (await), it does not loop.

Subagents

A tool can do its work as a subagent the user watches: a row under the tool call with its own transcript, the same as an agent's own subagents (agent-protocol.md, Subagents).

async run({ question }, ctx) {
  const sub = await ctx.subagent.start({ title: "Research", agent: "codex", model: "gpt-5.5", prompt: question });
  sub.step("Reading the docs");                       // a step row, and the "doing now" line
  const row = sub.tool({ name: "fetch", title: "Fetch the API page", kind: "fetch" });
  row.output("200 OK");
  row.end("completed");                               // or "failed", with content
  sub.text("The API has two endpoints.");             // streamed text; sub.reasoning(...) too
  sub.usage({ usedTokens: 1200 });
  sub.end({ status: "completed", summary: "Two endpoints" });
  return "The API has two endpoints.";
}

start({ title, name?, agent?, model?, effort?, prompt? }) resolves a handle once the host shows the subagent (agent names the agent it runs on; the row shows its name). The handle streams in order: text(delta), reasoning(delta), step(title), tool(call) (returns { id, update, output, end }), usage(usage), notice(message, level?), update({ title?, activity?, summary?, toolUses?, model?, effort? }), event(kind) for any of these as an agent event (such as one api.models.prompt streams), and end({ status, summary }), which the call's answer waits for. signal aborts when the user stops the subagent or the turn. Pass the handle to api.models.prompt({ ..., subagent }) to show a prompt's run in it (ui-plugins.md, Models and subagents).

The subagent lives as long as the call: a subagent the call leaves running ends with it (completed, failed or stopped with the call), and the handle shows nothing after that. Outside a chat (scope absent) the handle works and shows nothing (handle.shown is false). A call without a callId nests under the chat's running row of that tool, or execute in code mode.

The official subagents plugin (convergence/subagents, in the marketplace, not installed by default) is such a tool: spawn_subagent({ prompt, agent?, model?, effort?, title?, type? }) runs a subagent on any enabled agent and model, or a plugin's subagent type, and answers with its final text.

Approvals

Each agent treats plugin tools the way it treats its own tools of that kind. Claude Code asks before each call unless its permission mode allows it; Codex runs them without asking; OpenCode and ACP agents follow their own permission settings.

When changes reach a chat

A session gets the tools and rules that are on when it starts or is resumed. Codex keeps the tool set a thread started with for the thread's whole life; a plugin that changes its tools reaches new Codex chats only. In code mode the program always calls the current tools.

Commands as tools

Agents cannot run the app's commands (MARKETPLACE.md D50): no tool does it by default. A plugin that asks for both commands.run and tools.provide can offer commands as tools: its tool calls api.host.kernel("run_command", { id }) for the command, which runs in the plugin that owns it with that plugin's permissions. Its enable card lists both permissions, so a user who wants agents to run commands installs such a plugin on purpose:

api.tools.register({
  name: "run_command",
  description: "Runs one of the app's commands by id (see list_commands).",
  inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
  run: async ({ id }) => {
    const { ran } = await api.host.kernel("run_command", { id });
    return ran ? `ran ${id}` : `${id} did not run`;
  },
});

The protocol

The runtime answers two JSON-RPC methods for a plugin (process-plugins.md, The wire protocol):

Method Params Result
tools/list {} { tools: [{ name, description, inputSchema, outputSchema?, title?, annotations?, scope? }] }
tool/call { name, input, context: { workspaceId?, workspace?, agentId?, chatId?, callId? } } { content: [...], structuredContent?, isError? }

The shapes are MCP's Tool and CallToolResult. The host asks tools/list after initialize, and again after the notification host/tools.changed. A call the host gives up is followed by the notification tool/cancel { id }, the request id of the tool/call.

How a provider delivers them

For provider authors (agent-protocol.md): agent/create_session, agent/resume_session and agent/fork_session carry tools (what the session may call) and instructions (text to add to the agent's own instructions). The provider declares the tools in its agent's own way and passes each call to the host with host/tools.call { agentId, sessionId?, workspace?, name, input, callId? }, which answers a tool result. For an agent that takes tools only as an MCP server it connects to, a JavaScript provider asks the host to serve them (host/mcp.serve, which answers a loopback URL to hand the agent). The provider SDK's callHostTool makes the host/tools.call, and its handleMcp answers MCP messages an agent sends over its own channel (Claude Code).

Source: plugins/docs/tools.md in the Divergence repository, built with this site.