~/.praisonai/plugins/ and load it in one line.
Quick Start
1
Simple Usage
Create Load and run:
~/.praisonai/plugins/my_tools.py:2
With Configuration
Point the environment (or config file) at plugins and Agent construction wires them for you — no explicit Prefer to turn plugins on in code? Call
plugins.enable() call needed:plugins.enable(...) before creating the agent:Place plugins in
~/.praisonai/plugins/ (user-wide) or ./.praisonai/plugins/ (project-specific).discover_and_load_plugins() loads and registers plugins. To read plugin metadata without loading (e.g. to build a config-validation gate), use discover_plugins() and the capability manifest fields — see Static Capability Manifest.Selective Enable and Late Registration
Afterplugins.enable([names]), any plugin registered later stays disabled unless its name is in that list.
PluginManager.register(...) all inherit the same rule, because each path flows through register().
Widen or Reset the Allow-List
A manager created directly via
PluginManager() is not bound by the facade’s allow-list — every registration on it starts enabled. Only the singleton used by plugins.enable(...) carries the restriction.plugins.enable() with no argument (unrestricted) enables everything currently registered and every future registration — the late-registration rule only applies to a selective enable([list]).How It Works
plugins.enable() auto-calls wire_into_hook_registry(), which registers each enabled plugin’s lifecycle methods on the default hook registry the agent consults at runtime — and Agent init calls this for you when the env var or config file requests it (see Auto-Enable from Env or Config).
The hooks fire in this order around each agent run:
On Agent construction the plugin manager feeds more than the hook registry: a plugin declared PluginType.GUARDRAIL / POLICY / SKILL is routed into the Agent’s typed subsystems — GuardrailChain, PolicyEngine, and SkillManager — via _merge_plugin_subsystems():
Choosing How to Load Plugins
Pick the loading method that fits your setup.Enabling built-ins by name with
plugins.enable([...]) starts a selective allow-list — see Selective Enable and Late Registration for what happens to plugins you register afterwards.Plugin Locations
Security: Project-Plugin Trust Gate
A single-file plugin is the most privileged extension surface there is — it can hook every lifecycle event and intercept every tool call. So a plugin dropped into a project’s./.praisonai/plugins/*.py (for example, one that arrived with a cloned repository) does not run until you opt in. This mirrors PRAISONAI_ALLOW_LOCAL_TOOLS for tools: a cloned repo carrying a malicious .praisonai/plugins/exfil.py stays inert by default.
Open the gate with an environment variable (accepts true/1/yes/on):
.praisonai/config.yaml:
This gate decides whether a plugin loads at all. A separate boundary decides whether a loaded plugin may read or rewrite conversation content — see Security: Conversation-Content Capability Gate.
Security: Conversation-Content Capability Gate
A plugin that isn’t bundled must be explicitly grantedallow_conversation before it can read or rewrite prompts, responses, and outbound messages — its tools still run either way.
A third-party plugin’s tools still register and fire; only the eight conversation-content hooks are withheld until you grant access.
Which hooks are gated
These eight hooks carry — and can rewrite — the user’s prompt, the model’s response, or an outbound reply, so they are default-deny for third-party plugins:Grant a third-party plugin access
Addallow_conversation: true to the plugin’s own option block in .praisonai/config.yaml:
allow_conversation is framework-reserved — the manager interprets it and does not pass it through to the plugin’s on_config. Every other key in the block still reaches on_config as before.What “first-party” means
First-party plugins — those bundled inside the installedpraisonaiagents/ package on disk — are granted conversation access automatically. This is a different notion from the location trust levels (project vs user-global vs entry-point) of the Project-Plugin Trust Gate.
A plugin is first-party only when both hold:
- its class’s module name is
praisonaiagentsor starts withpraisonaiagents., and - that module’s real on-disk file resolves to a path inside the installed
praisonaiagents/package directory.
Interaction with the project-plugin trust gate
The two gates are complementary. A project-local single-file plugin authorised to load viaPRAISONAI_ALLOW_PROJECT_PLUGINS=true is still not first-party — it must also carry allow_conversation: true in its config block before it sees any conversation-content hook.
Tools and channels are never gated
An ungranted plugin still registersget_tools(), before_tool, after_tool, before_tool_definitions, on_permission_ask, channel adapters, on_init / on_shutdown, session_start / session_end, on_error, on_config, and on_auth. Only the eight conversation-content hooks above require the grant.
Which plugins see prompt content
Inspect the Plugin Registry
get_plugin_registry() returns a truthful, unified view of every discoverable plugin from three real sources — no hardcoded entries. It reads without a running agent, so a CLI or health check can render it directly.
{name, version, description, source, enabled, hooks, requests_conversation, conversation_granted} (single-file entries also include path). The source field records provenance:
enabled reflects both the live manager state and the persisted config allow-list, so it renders correctly even before any agent starts.Reload Plugins at Runtime
Edited a plugin, or dropped a new one in, and want it live in the current process? Runpraisonai plugins reload — it forces rediscovery and rewires hooks so newly-added or edited plugins take effect without a restart. See the Plugins CLI.
Hook Plugin Example
Create~/.praisonai/plugins/my_logger.py:
~/.praisonai/plugins/tool_sandbox.py to filter advertised tools:
Typed Plugins (Guardrail / Policy / Skill)
DeclaringPluginInfo(plugin_type=PluginType.GUARDRAIL) (or POLICY / SKILL) moves a plugin off the generic lifecycle-hook path and into the Agent’s typed subsystem. A guardrail plugin then sees the raw tool call and the raw tool result — the blind spot the after_llm hook could never cover, because after_llm only ever saw the model’s paraphrase.
On Agent.__init__, right after _setup_guardrail() runs and _skills / _policy are set, _merge_plugin_subsystems() collects the typed plugins and folds them in:
Each collector dispatches on the declared plugin.info.plugin_type and iterates enabled plugins only, snapshotting (name, plugin) pairs under the manager’s lock:
Quick Start
1
Guardrail plugin — redact a raw tool result
A The same effect through
PluginType.GUARDRAIL plugin returns a guardrail object from as_guardrail(). The object joins the Agent’s GuardrailChain and sees validate_tool_result — the raw tool output, before the model paraphrases it.after_llm (the pre-PR pattern) could not intercept the raw tool result — only the model’s paraphrase — so a secret leaked whenever the model quoted a tool verbatim or a downstream tool consumed the raw output.2
Policy plugin — add execution rules
A
PluginType.POLICY plugin returns Policy objects from get_policies(). They are added to the Agent’s PolicyEngine (one is created if the Agent has no policy=).3
Skill plugin — contribute loadable skills
A
PluginType.SKILL plugin returns filesystem path strings from get_skills(). They are appended to the Agent’s _skills so SkillManager loads them as real SKILL.md skills.Composition and precedence rules
These rules are locked in by the PR’sTestPluginTypedSubsystems suite:
User Interaction Flow
A PII guardrail plugin redacts a secret from the raw tool result — before the model or the user sees it:Each typed collector returns
[] when no plugin declares the matching type, and each merge step early-outs — zero cost when unused. See Guardrails from Plugins, Policies from Plugins, and Skills from Plugins for how each subsystem receives the contribution.Backward compatibility
- Every new base method (
as_guardrail(),get_skills(),get_policies()) is a no-op by default — existingPluginsubclasses that don’t override them behave identically. - The default
PluginInfo.plugin_typeisPluginType.HOOK, so existing plugins that don’t set the field are not collected by the typed collectors and keep routing through the generic lifecycle-hook path. - No new required parameters on
Agent(...)orPlugin(...).
Tool Plugins (subclass Plugin with get_tools())
Subclass Plugin, return callables from get_tools(), register once — every agent in the process can call them.
A class-based PluginType.TOOL plugin’s get_tools() output is merged into self.tools during Agent.__init__ — you do not call plugins.enable(), touch PluginManager.get_all_tools(), or pass anything special to Agent(...). Registering the plugin marks it enabled in the manager, and that is enough for its tools to reach every subsequent agent.
Quick Start
1
Register a tool plugin in code
Return callables from
get_tools(), register once, and the tool reaches the agent — no plugins.enable(), no tools=[...].2
Load the bundled plugin by name
The reference
basic_tools plugin ships in the separate praisonai-plugins package. Install it, enable it once, and every subsequent agent gains its tool.User Interaction Flow
Before this wiring, an agent asked for a random number would fabricate one. Now it actually calls the plugin’s tool:Precedence and Safety Rules
The collision rule in code — the agent’s own tool always wins:
@tool vs Plugin.get_tools() vs FunctionPlugin
Two ways to add a tool from a plugin — pick the one that matches how the tool is shipped.
The bundled
basic_tools plugin in the separate praisonai-plugins package is the canonical PluginType.TOOL example — its get_tools() returns [random_number], and that tool now reaches every agent end-to-end.Static Capability Manifest
Declare a plugin’s capabilities in its header so onboarding, doctor and config validation can read them without importing the plugin’s runtime. Add four optional fields to the header — they parse as plain text, so the plugin’s runtime is never imported to read them. Create~/.praisonai/plugins/telegram_channel.py:
Field reference
Read the manifest from code
discover_plugins() scans plugin directories and returns metadata dicts — the plugin body is never imported.
Omitted fields are absent from the raw
discover_plugins() dict, so read them with .get(key, []). PluginMetadata.to_dict() normalizes them to empty lists.Rules
- Fields are optional — omitting them yields empty lists (safe default).
- Field keys are case-insensitive —
Channels,channels, andCHANNELSall work. - Aliases:
Provides=Provides Tools=Tools;Config=Config Schema;Auto Enable When Configured=auto_enable_when_configured. - The plugin file’s runtime is never imported to read these fields — safe to scan untrusted plugin directories for a capability inventory.
Do I need a manifest?
Lifecycle-Method Plugins
SubclassPlugin and override a lifecycle method to transform prompts, messages, or responses. Call plugins.enable() — or set PRAISONAI_PLUGINS=true / [plugins] enabled = true and let Agent construction wire it — to activate the method at runtime.
Create ~/.praisonai/plugins/pii_redactor.py:
Plugin Lifecycle Observability
plugin.on_init and plugin.on_shutdown are the plugin’s own callbacks — they run on the plugin being registered or unregistered.
To have a different plugin (a metrics collector, an audit log, a gateway health monitor) observe the plugin set changing at runtime, subscribe to the ON_INIT / ON_SHUTDOWN hook events. They are emitted from PluginManager.register() / PluginManager.unregister() immediately after the plugin’s own callback runs.
registry.has_hooks(...)).
See Hook Events → Plugin Lifecycle Events for the payload reference.
Session & Error Lifecycle Plugins
Overridesession_start, session_end, on_error, on_config, or on_auth to react to session boundaries, errors, config, and credential resolution.
Choosing a lifecycle event
Observe sessions
session_start and session_end observe when a session opens and closes.
~/.praisonai/plugins/session_logger.py:
Observe errors
on_error observes errors during a run — use it to log without changing behavior.
~/.praisonai/plugins/error_reporter.py:
Rewrite config
on_config returns a dict to rewrite runtime configuration in place.
~/.praisonai/plugins/config_defaults.py:
Inject credentials
on_auth returns a dict of credentials — the bridge writes them back even when credentials starts as None, so first-time injection works.
~/.praisonai/plugins/token_injector.py:
Gateway, Schedule & Runtime Lifecycle Plugins
Overridegateway_start, schedule_trigger, before_compaction, subagent_stop, model_fallback (and their siblings) to observe the framework’s runtime lifecycle from a plugin — no low-level HookRegistry needed.
Choosing a lifecycle event
Observe gateway lifecycle
gateway_start and gateway_stop fire when the gateway (BotOS) starts and stops.
~/.praisonai/plugins/gateway_watch.py:
Observe schedule lifecycle
schedule_add, schedule_remove, and schedule_trigger fire when a job is persisted, deleted, or fires.
~/.praisonai/plugins/schedule_watch.py:
Observe compaction
before_compaction sees the pre-compaction snapshot; after_compaction sees the post-compaction summary.
~/.praisonai/plugins/compaction_watch.py:
This is the observability path. For a memory backend that reacts to compaction with an actual write (summary, table switch), subclass
Memory — see Memory Lifecycle Hooks. The Memory callbacks mutate; the plugin method observes.Observe subagent completion
subagent_stop fires when a spawned subagent reaches a terminal state — useful for audit and metrics.
~/.praisonai/plugins/subagent_watch.py:
Observe model fallback
model_fallback fires when the runtime falls back to a secondary model — log the primary_model → fallback_model transition and its reason.
~/.praisonai/plugins/fallback_watch.py:
Observe kanban board changes
Declare one or more ofKANBAN_TASK_CREATED, KANBAN_TASK_MOVED, KANBAN_TASK_DONE and define the matching kanban_task_*(context) methods.
~/.praisonai/plugins/kanban_watch.py:
Kanban events have no base no-op method on
Plugin. A plugin doesn’t need the base class to define them — the bridge treats “no base counterpart” as a genuine override, so a declared KANBAN_TASK_* hook with a matching kanban_task_* method is wired.How the declaration-driven bridge works
APlugin method is bridged when both of these are true:
- The matching
HookEventis listed inPluginInfo.hooks. - The method is either a genuine override of the base no-op, or the event has no base counterpart at all (e.g.
kanban_*).
info.hooks without overriding the method registers nothing (no dead hooks). Overriding the method without declaring the hook also registers nothing — the runtime is strictly declaration-driven so authors keep control of what fires.
The bundled
GatewayForensicsPlugin in praisonai-plugins fires as designed once this bridge is present. Operators upgrading past this fix will see their forensics plugin start emitting gateway/schedule/kanban signals that were previously silently inert.Message-Lifecycle Plugins
Overridebefore_message, after_message, message_sent, or message_undelivered to react to every hop of a message’s lifecycle — per-user policy, per-channel redaction, delivery telemetry, or dead-letter escalation.
Choosing a message hook
Enriched payload keys
Every message-lifecycle method receives (at most) these keys:Absent fields are skipped — the bridge does not fabricate empty strings for keys the concrete input doesn’t carry. Read fields with
.get(key), not subscripting, so a plugin written against inbound input degrades cleanly on outbound events.React per user, per channel, and per delivery
Lead with the outcome: rate-limit a noisy sender, count deliveries per channel, or escalate a dead letter.message_undelivered is the counterpart to the plain MESSAGE_UNDELIVERED hook already covered in Undelivered Messages and Gateway → Undelivered Notice — this hook exposes the same signal at the plugin surface, so Plugin subclass authors don’t need to reach for the raw HookRegistry.Function-Based Plugins (FunctionPlugin)
FunctionPlugin wraps plain functions into a plugin without subclassing — useful for one-off hooks or when the whole plugin is a single callable.
When to reach for it
Quick Start
Register aFunctionPlugin on the plugin manager, then start the agent — the callable runs before the agent sees the message.
Constructor
Hook dispatch table
Each key inhooks={} is a PluginHook enum value; the callable’s return value drives the effect below.
Message-lifecycle examples
Lead with the outcome: rate-limit a channel, log every delivery, or capture permanent failures.FunctionPlugin’s message-lifecycle dispatch (MESSAGE_RECEIVED, MESSAGE_SENDING, MESSAGE_SENT, MESSAGE_UNDELIVERED) is a recent addition (commit 857ebbe) — earlier versions silently no-op’d these four keys, so users had to subclass Plugin.For the full payload contract (identity keys, per-event availability,
error / notice_delivered special-cases), see Message-Lifecycle Plugins — FunctionPlugin and Plugin subclasses share the exact same _message_payload bridge.Auto-Enable from Env or Config
Agent construction auto-enables plugins when the environment or config file requests it — no explicitplugins.enable() call needed.
Set the env var, then any Agent(...) wires plugins before it runs:
.praisonai/config.toml:
plugins.maybe_enable_from_config(), which reads the env var and config file, then runs enable(get_enabled_plugins()) exactly once per process.
Precedence: explicit plugins.enable(...) > PRAISONAI_PLUGINS env var > [plugins] in config.yaml / config.toml > disabled.
maybe_enable_from_config() is idempotent and runs at most once per process, so instantiating multiple agents is safe — plugins are wired a single time.Suppress Plugins for One Run (Pure Mode)
The inverse of auto-enable — skip external-plugin discovery for a single run without touching persisted state. Pass--pure (or --no-plugins) to run, chat, or code:
disabled=True > PRAISONAI_NO_PLUGINS env var. The CLI flag is scoped and restored on return, and never mutates .praisonai/config.yaml.
See Pure Mode for the full guide.
Configure Plugins from .praisonai/config.yaml
Turn plugins on and hand each one its own options from a single YAML file — no code.
~/.praisonai/config.yaml:
app.py:
Agent construction auto-loads the config, enables plugins, and hands
pii_guardrail its {redact: [email, phone]} dict via on_config. Nothing else to wire up.Where the config file is found
The first file found wins — TOML and YAML have equal standing; whichever appears first in the search stops the walk.
The four shapes of enabled
The JSON config schema advertises
boolean and array for enabled; the loader also accepts a bare string at runtime for TOML users who prefer enabled = "pii_guardrail".A bare
enabled: true is now preserved — enabling a plugin while it is set is a no-op (leaving “all enabled” intact). You cannot run praisonai plugins disable X while enabled: true: the CLI refuses with a clear error, since collapsing “all” to an allow-list would silently disable every other plugin. Set plugins.enabled to an explicit list of names first, then disable individual plugins.Per-plugin option blocks
Reserved keys (enabled, auto_discover, directories, allow_project_plugins) configure the plugin system; any other key whose value is a mapping is treated as a per-plugin option map delivered to that plugin’s on_config hook. Inside a per-plugin block, allow_conversation is also framework-reserved — the manager reads it to gate conversation-content hooks and does not forward it to on_config.
allow_project_plugins: true opens the project-plugin trust gate so single-file plugins under ./.praisonai/plugins/ load. The PRAISONAI_ALLOW_PROJECT_PLUGINS env var takes precedence over this config key.An explicit top-level
enabled: false wins over any per-plugin block. Omit enabled at the top level and per-plugin blocks decide.Reading options inside a plugin — on_config
apply_plugin_options() invokes plugin.on_config(options) for every enabled plugin that has a configured option map — errors in a single plugin are logged and do not abort delivery to the others.
Create ~/.praisonai/plugins/pii_guardrail.py:
on_config fires once per enable — the reserved enabled flag (if present in the plugin block) is preserved so plugins can read it too.Deliver options from code — options_by_name
Skip the file and hand each plugin its options directly. plugins.enable() takes an options_by_name map that it delivers to each plugin’s on_config hook:
.praisonai/config.yaml calls this for you — maybe_enable_from_config() runs enable(get_enabled_plugins(), options_by_name=get_plugin_options()) under the hood. Call get_plugin_options() yourself to read the per-plugin maps the loader parsed.Choose a plugin config surface
Precedence
Explicitplugins.enable(...) in code > PRAISONAI_PLUGINS env var > [plugins] in config.yaml / config.toml > disabled by default.
Edge cases
- PyYAML missing — YAML configs silently return empty (debug log only); install
pyyamlto enable. - Unknown reserved key — the wrapper resolver’s typo-suggestion validator only checks against
{enabled, auto_discover, directories, allow_project_plugins}; per-plugin blocks (dicts) are always accepted. - Removed plugin block on reload — the manager stores options with
replace=Trueby default, so a block dropped from a later config no longer delivers stale options. - Plugin
on_configraises — logged as a warning; delivery to other plugins is not aborted.
Config-write single source of truth
praisonai plugins enable/disable write to the same file the runtime reads — no separate JSON. The write target is resolved in this order:
praisonai plugins disable <name> unloads a single-file plugin’s tools as well as its hooks — unload_plugin(module_name) removes the functions that module contributed (tracked via a pre-exec registry snapshot), so disable truly removes the plugin’s functionality for the rest of the run without touching tools owned by another plugin or the core.How the Bridge Works
plugins.enable() auto-calls wire_into_hook_registry() — no manual step. Only lifecycle methods a plugin actually overrides (or declares in PluginInfo.hooks) are bridged, so a plugin with one guardrail never fires on every event. Return a new value and the bridge writes it back onto the payload in place. Errors in a lifecycle method are non-fatal, and plugins.disable([...]) calls unwire_from_hook_registry(name) so the plugin truly stops firing.
The five
before_* methods (before_agent, before_llm, before_tool, before_tool_definitions, before_message) also accept a deny/block decision instead of a rewrite — see Blocking Plugins. after_tool additionally accepts a rewrite or block — see Redact / Block Tool Output.🔐 marks the eight conversation-content hooks. For a third-party plugin they fire only after
allow_conversation: true; first-party plugins are granted automatically.on_config and on_auth write their returned dict back onto the payload even when the target attribute starts as None — so a plugin can inject credentials the first time they’re requested, not only edit an existing dict.
after_tool is now a redaction / block seam, not an observer (PraisonAI PRs #3968 / #3969). A Plugin.after_tool return value is written back to the effective tool result before it reaches the model — mirroring before_tool (arg rewrite) and after_llm (response rewrite). Return a scrubbed value to redact, PluginDecision.deny("reason") to suppress, or raise GuardrailBlocked("reason") for the same effect via the exception form. See Redact / Block Tool Output.Reference Plugins in praisonai-plugins
Batteries-included plugins ship in the separate praisonai-plugins package — install once, then enable by name.
cli_backend_tracer overrides Plugin.cli_backend_execute(context) and logs every CLI backend delegation to the standard praisonai logger — with the prompt already redacted, so the log stream is safe to ship to an aggregator.
CliBackendExecuteInput payload reference.
Blocking Plugins (Guardrails & Policies)
Guardrail and policy plugins can now stop an action before it happens, not just rewrite it — refuse a dangerous tool call, drop a spam message, or decline an LLM request.Quick Start
1
Block a tool call
Return
PluginDecision.block(reason) from before_tool to skip a forbidden tool.2
Refuse an LLM request
Return
PluginDecision.deny(reason) from before_llm and the agent returns "[LLM request blocked by hook: <reason>]" without calling the model.3
Drop an inbound message
Return
PluginDecision.deny(reason) from before_message to drop a message before the agent sees it.Three Ways to Block
The same block, written three ways — pick the one that reads cleanest in your code.PluginDecision.deny(reason) and PluginDecision.block(reason) both stop the action (is_denied() is True for each); allow(reason=None) is an explicit no-op. GuardrailBlocked(reason: str = "Blocked by guardrail plugin") is caught by the bridge and converted to a block. It may be raised from any before_* method and from after_tool — the tool has already run there, but the output is suppressed before it reaches the model (see Redact / Block Tool Output).
Where Blocks Fire
The fivebefore_* methods and after_tool can block; the remaining after_* / observational methods cannot.
How a Block Flows
Which Style Should I Use?
Ship a Plugin as a pip Package
Register your plugin class in thepraisonai.plugins entry-point group in pyproject.toml:
plugins.enable()) auto-discovers and bridges it — no user code changes needed.
Once installed, verify the plugin registered with praisonai plugins add <package> --dry-run — see plugins add.
A shipped plugin cannot replace a built-in name. PraisonAI’s registries subclass the base
PluginRegistry, so since PR #4176 any entry point whose name (case-insensitive) matches a shipped built-in is skipped and the built-in is kept (a DEBUG line notes the collision). Pick a name unique to your package. Runtime register(...) is the deliberate override path. See Plugin Precedence.CLI Commands
praisonai plugins list shows every plugin with its source (entry_point:<dist>, registered, or single_file). enable/disable persist to the config the runtime reads, reload picks up edits without a restart, and doctor diagnoses issues. See the full reference on the Plugins CLI page.
Configuration Options
OnlyPlugin Name is required; every other field is optional.
Best Practices
Keep plugins single-purpose
Keep plugins single-purpose
One file per concern — weather tools, logging, or guardrails, not all three.
Call discover_and_load_plugins() once
Call discover_and_load_plugins() once
Load before creating the agent so tools and hooks register globally.
Reference tools by name
Reference tools by name
Pass
tools=["get_weather"] — the string must match the @tool function name.Use plugins.enable for built-ins
Use plugins.enable for built-ins
Enable
logging and metrics without writing plugin files.Call plugins.enable() to activate lifecycle methods
Call plugins.enable() to activate lifecycle methods
Tools and guardrails work without
plugins.enable(). But lifecycle-method plugins (subclasses of Plugin that override before_llm, after_llm, and so on) only fire after they are wired into the runtime hook registry. Call plugins.enable() explicitly, or set PRAISONAI_PLUGINS=true / [plugins] enabled = true and Agent construction wires them automatically.Reach for PluginDecision before HookResult
Reach for PluginDecision before HookResult
PluginDecision.deny(reason) reads as one line and only needs the plugin import. Use HookResult.deny(reason) only when you already import it for another reason, and raise GuardrailBlocked(reason) only when you’re inside a validator that already raises on failure. Prefer block over deny when the action is categorically forbidden; use deny when it’s contextually refused (this input, this user, this time).Thread Safety
The process-wide plugin manager returned byget_plugin_manager() is safe to share across concurrently running agents.
All lifecycle methods — register, unregister, enable, disable, execute_hook — synchronise on a single RLock, so concurrent registration and hook firing cannot collide. See Thread-Safe Agent State → PluginManager (PR #5204) for the mechanism.
Related
Hooks
Hook events and the HookRegistry API
Toolsets
Create and register custom agent tools
Plugins CLI
List, enable, disable, reload, and diagnose plugins
Config File
Turn plugins on from
[plugins] in config.tomlTool Discovery
How Agent resolves tool names at runtime
Guardrails
Validate agent output — automatic retry on failure
Redact Tool Output
Scrub secrets or block tool results before the model sees them
Conversation-Content Gate
Grant third-party plugins access to prompts and responses

