Plugins

A plugin is an installable package whose default export receives a typed PluginContext. It extends TAI without forking the monorepo or sharing a runtime copy of @tailored-ai/core.

SurfaceAddsRegister with
ToolsTools an agent can callctx.tools.register(id, factory)
ChannelsDiscord-like inbound and outbound transportsctx.channels.register(id, factory)
ProvidersModel and embedding backendsctx.providers.register(...), ctx.embeddings.register(...)
StorageMemory, task, and repository backendsctx.memoryBackends.register(...), ctx.taskBackends.register(...), ctx.repoBackends.register(...)
ExecutionSandboxes, workflow steps, and time providersctx.sandboxBackends.register(...), ctx.stepExecutors.register(...), ctx.timeProviders.register(...)
Product surfaceUI providers, HTTP routes, and chat commandsctx.uiProviders.register(...), ctx.http.register(...), ctx.commands.register(...)
BehaviorRuntime event subscribersctx.events.on(name, handler)

Skills, prompts, and knowledge bases use the resource system rather than the plugin factory registries. A plugin can still install or expose those resources.

Shape of a plugin

ts
// my-tai-plugin/src/index.ts
import type { Plugin, PluginMeta } from "@tailored-ai/core";
import { WeatherTool } from "./weather.js";

const plugin: Plugin = (ctx) => {
  ctx.tools.register("weather", (config) => {
    const cfg = config.tools.weather;
    if (!cfg?.enabled) return [];
    return [new WeatherTool(cfg)];
  });
};

export const meta: PluginMeta = {
  name: "Weather tools",
  description: "Current conditions and forecasts.",
  registers: [{ kind: "tool", id: "weather", configKey: "tools.weather" }],
};

export default plugin;

The Plugin import is type-only, so it disappears from the compiled package. The host imports the package, calls its default export with the current PluginContext, and retains an optional disposer returned by the plugin.

Installing

bash
tai plugin install @some-author/tai-plugin-slack

The plugin lands in <TAI_HOME>/plugins/ and the CLI appends it to config.yaml's plugins: list for you (comments preserved; pass --no-save to opt out). The list accepts bare names or objects:

yaml
plugins:
  - "@some-author/tai-plugin-slack"
  - module: "@me/tai-plugin-linear"
    config:
      api_token: ${LINEAR_TOKEN}

The CLI imports each module at startup and invokes its default register function. Failures are logged and the next plugin is attempted—one broken plugin does not take down the others. Legacy import-side-effect plugins remain supported, but register(ctx) is the current contract.

The optional per-entry config bag is available to that plugin as ctx.config. Factories can also read the shared runtime config under normal tools.*, channels.*, and providers.* blocks.

For embedders (anyone constructing AgentRuntime directly rather than running tai), call loadPlugins(config, importer, { context }) before runtime construction so provider, tool, and channel factories are present when the runtime resolves them. The importer must resolve from the host application. Create the event bus before both the plugin context and runtime so subscribers receive the runtime's events. The CLI startup in packages/cli/src/index.ts is the canonical wiring sequence.

Metadata and config validation

Two optional named exports sit next to the default register function.

meta describes the plugin for UIs and error hints — GET /api/plugins returns it, and registers documents the link between the plugins: entry (which loads code) and the config blocks (which turn features on):

ts
import type { PluginMeta } from "@tailored-ai/core";

export const meta: PluginMeta = {
  name: "AWS Bedrock provider",
  description: "Bedrock-hosted models via the Converse API.",
  registers: [{ kind: "provider", id: "bedrock", configKey: "providers.bedrock" }],
};

validateConfig checks the plugin's own config blocks at load time. Core's validateConfig knows nothing about plugin config shapes on purpose — the plugin owns them:

ts
import type { AgentConfig } from "@tailored-ai/core";

export function validateConfig(config: AgentConfig): string[] {
  const cfg = config.providers.bedrock as BedrockConfig | undefined;
  if (cfg && !cfg.defaultModel) return ["providers.bedrock.defaultModel is empty"];
  return [];
}

Warnings print at startup alongside core's config warnings and appear on GET /api/plugins. A validator can't veto startup; factories still fail fast for hard errors. Both exports are type-only contracts — like the Plugin type itself, they add zero runtime dependency on core.

First-party plugins

These ship from the monorepo as separate npm packages:

PackageWhat it addsHow to use today
@tailored-ai/browser-mediatorThe browser_mediator tool with egress allow-list, vault refs, always-HITL gates.Already a dependency of @tailored-ai/core. Enable via tools.browser_mediator: { enabled: true }.
@tailored-ai/trusted-actionsHITL approval gateway plus the request_action tool.Install separately. Configure under tools.request_action.
@tailored-ai/provider-bedrockThe bedrock model provider (AWS Bedrock Converse API).Install, configure providers.bedrock, select with agent.defaultProvider: bedrock.
@tailored-ai/provider-openrouterThe openrouter model provider (OpenAI-compatible).Install, configure providers.openrouter, select with agent.defaultProvider: openrouter.
@tailored-ai/provider-anthropicThe anthropic model provider (Messages API and prompt caching).Install, configure providers.anthropic, select with agent.defaultProvider: anthropic.
@tailored-ai/provider-openaiThe openai model provider (chat completions and reasoning-model request shaping).Install, configure providers.openai, select with agent.defaultProvider: openai.
@tailored-ai/channel-slackThe slack channel (Bolt, Socket Mode).Install, configure channels.slack.
@tailored-ai/google-toolsgmail, google_calendar, google_drive tools (via the gog CLI).Install, enable under tools.gmail etc.

Future first-party plugins under consideration: Telegram channel, Linear task backend, Notion knowledge base.

Patterns for plugin-friendly code

  • Take all configuration via the factory's config argument. No reading from process.env directly. Let the user route env vars through YAML.
  • Fail fast in the factory. If required config is missing, throw with a useful message. The startup loop catches and logs.
  • Dynamic-import heavy peer dependencies. If your plugin uses playwright, await import("playwright") inside the connect handler, not at the top of the file. npm install your-plugin shouldn't drag Chromium along unless the user opts in.
  • Don't reach into TAI internals. Use the public exports of @tailored-ai/core. The runtime shape may change between minor versions; public exports won't.

Why register instead of fork?

Forking the monorepo to add a channel or task backend is heavy. Wrapping shell commands in YAML custom tools is limited. The registries fill the gap: write idiomatic TypeScript, ship to npm, your users import your package once and the registrations land. The TAI monorepo doesn't grow. You don't carry a fork.

Where to read next