Hardcoding if tool_name == ... does not scale past a handful of tools.
A plugin architecture lets each capability register a schema, handler, and metadata while the ReAct loop stays a thin dispatcher.
This recipe stays framework-free and portable to later hosts (native SDKs or LangGraph adapters).
Define a ToolPlugin contract (name, description, JSON Schema, handler, risk flags), register plugins into a registry that builds model tool specs and dispatches calls, and filter the active set per route, tenant, or role before each run.
For most product agents, explicit registration is the right default.
Treat MCP as another plugin source that translates remote tools into the same ToolPlugin shape.
When selection quality drops or descriptions no longer fit context budgets. Split profiles or use a router agent with small specialist catalogs.
Should plugins be async?
If your host is async and tools do I/O, yes. Keep the registry API consistent (async def handle or a thread offload policy) so the loop does not block.
How do I version a breaking tool change?
Introduce refund_order_v2, migrate prompts/evals, then remove v1 after a deprecation window. Do not silently change required args.
Where do human approvals plug in?
In the dispatcher: if risk is write/dangerous and policy requires approval, return error_type=needs_approval or pause the run before handle.
Can plugins live in another package/repo?
Yes. Publish an internal package that exports register(registry) and pin versions. Review supply chain like any dependency.
How do plugins interact with circuit breakers?
Use breaker_key metadata so the dispatcher wraps remote calls uniformly. See the circuit breakers article in this section.
Do I need a plugin system for three tools?
A tiny dict of callables is fine. Introduce a formal registry when allowlists, risk metadata, or multiple profiles appear.
How should errors be shown to the model?
Stable JSON with error_type and a short message. Avoid huge stack traces in the transcript.
Is this the same as dependency injection frameworks?
It is a narrow registry for tools, not a general DI container. Keep it small on purpose.
How do I test a plugin?
Call handle with a fake AuthContext and fixed args. Separately test registry policy denials without invoking real side effects.