Extensibility

Tools

Add new actions the model can call. A plugin tool lands in the same catalog as the Assistant's built-in tools, so the model picks it up with no extra wiring.

Plugins are in beta. The plugin API (@vellumai/plugin-api) is not yet stable and can change between releases. Pin the peerDependencies range your plugin declares, and expect breaking changes until we cut a 1.0. Tool field names and context shapes can change with it.

A tool is a default-exported object from tools/<name>.ts. The loader derives the model-visible tool name from the file basename, so tools/example.ts becomes the example tool. Plugin tools register in the same catalog as built-in tools and are offered to the model through the standard tool-calling interface.

What a tool is

Where a hook runs your code at a fixed point in a turn, a tool is something the model chooses to call. You describe what it does and what arguments it takes, and the model decides when to invoke it. When it does, the Assistant runs your execute function and feeds the result back into the turn.

Every field on a tool definition is optional. The loader fills documented defaults for anything you omit, so export default {} is a valid (if useless) tool. A broken or misconfigured tool never blocks the rest of the plugin from loading; the problem surfaces at call time instead.

Tool reference

These are the fields a tool definition can set. Names and types come from ToolDefinition in @vellumai/plugin-api.

FieldTypeDefaultDescription
namestringFile basenameName the model sees when calling the tool. Loaders default to the source file basename, so tools/example.ts becomes example. Only set this to override the file-derived name.
descriptionstring""Human-readable description shown to the model in the tool catalog. This is how the model decides when to call the tool, so write it for the model.
input_schemaobject (JSON Schema)Empty object schemaJSON Schema describing the tool's input arguments. The model is constrained to this shape when it calls the tool.
defaultRiskLevel"low" | "medium" | "high""medium"Author-asserted risk band that drives default permission gating. The medium default prompts the user, then allows on first invocation.
categorystringNoneTool category used for channel-scoped allowedToolCategories enforcement.
executionTarget"sandbox" | "host"Resolved automaticallyWhere the tool runs: the sandbox (assistant container) or the host (guardian device, via proxy).
execute(input, ctx) => Promise<ToolExecutionResult>Unimplemented errorImplementation invoked when the model calls the tool. When omitted, the loader synthesizes a result that reports the tool as unimplemented.

The execute context

execute(input, ctx) receives the model-supplied input (validated against your input_schema) and a ToolContext. It returns a ToolExecutionResult. The context carries the fields most tools reach for:

FieldTypeDescription
conversationIdstringConversation this tool invocation belongs to.
workingDirstringWorking directory the daemon was launched from.
requestIdstring?Per-turn request id for cross-component log correlation.
signalAbortSignal?Cooperative cancellation. Check signal.aborted periodically, or forward it to fetch and child-process options.
onOutput(chunk: string) => void?Incremental-output callback for streaming tools. Fall back to returning the full result in content when it is absent.
isInteractiveboolean?True when an interactive client is connected (not just a no-op callback).

And the result is what the model sees back:

FieldTypeDescription
contentstringText result shown to the model in the tool-result block. An empty string is valid.
isErrorbooleanWhen true, the agent loop treats content as an error and may surface it or retry.
statusstring?Short status message for client display, such as "truncated" or "timed out".
yieldToUserboolean?When true, the loop returns control to the user after this result instead of making another model call.
contentBlocksContentBlock[]?Rich content blocks (for example images) to include alongside the text result.

Anatomy of a tool

One tool per file, default-exported. The filename becomes the tool name, so an example tool is tools/example.ts:

// tools/example.ts
import type { ToolContext, ToolExecutionResult } from "@vellumai/plugin-api";

export default {
  description:
    "Search saved notes for a phrase. Use this when the user asks what they told you to remember.",
  defaultRiskLevel: "low" as const,
  input_schema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Text to search for." },
    },
    required: ["query"],
  },
  async execute(
    input: Record<string, unknown>,
    ctx: ToolContext,
  ): Promise<ToolExecutionResult> {
    const query = String((input as { query?: unknown }).query ?? "").trim();
    if (query.length === 0) {
      return { content: "error: query must be non-empty", isError: true };
    }
    // ctx.conversationId - current conversation
    // ctx.signal         - forward to fetch() / spawn() for cancellation
    return { content: `searched ${ctx.conversationId} for ${query}`, isError: false };
  },
};

Types come from @vellumai/plugin-api, the only supported contract.

The Personal AI you were promised

GET STARTED