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 astools.<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.