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.
| Surface | Adds | Register with |
|---|---|---|
| Tools | Tools an agent can call | ctx.tools.register(id, factory) |
| Channels | Discord-like inbound and outbound transports | ctx.channels.register(id, factory) |
| Providers | Model and embedding backends | ctx.providers.register(...), ctx.embeddings.register(...) |
| Storage | Memory, task, and repository backends | ctx.memoryBackends.register(...), ctx.taskBackends.register(...), ctx.repoBackends.register(...) |
| Execution | Sandboxes, workflow steps, and time providers | ctx.sandboxBackends.register(...), ctx.stepExecutors.register(...), ctx.timeProviders.register(...) |
| Product surface | UI providers, HTTP routes, and chat commands | ctx.uiProviders.register(...), ctx.http.register(...), ctx.commands.register(...) |
| Behavior | Runtime event subscribers | ctx.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
// 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
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:
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):
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:
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:
| Package | What it adds | How to use today |
|---|---|---|
@tailored-ai/browser-mediator | The 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-actions | HITL approval gateway plus the request_action tool. | Install separately. Configure under tools.request_action. |
@tailored-ai/provider-bedrock | The bedrock model provider (AWS Bedrock Converse API). | Install, configure providers.bedrock, select with agent.defaultProvider: bedrock. |
@tailored-ai/provider-openrouter | The openrouter model provider (OpenAI-compatible). | Install, configure providers.openrouter, select with agent.defaultProvider: openrouter. |
@tailored-ai/provider-anthropic | The anthropic model provider (Messages API and prompt caching). | Install, configure providers.anthropic, select with agent.defaultProvider: anthropic. |
@tailored-ai/provider-openai | The openai model provider (chat completions and reasoning-model request shaping). | Install, configure providers.openai, select with agent.defaultProvider: openai. |
@tailored-ai/channel-slack | The slack channel (Bolt, Socket Mode). | Install, configure channels.slack. |
@tailored-ai/google-tools | gmail, 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
configargument. No reading fromprocess.envdirectly. 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-pluginshouldn'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
- Configuration → Plugins — config-block syntax reference.
- Extending in code — what each registry's interface looks like.