Plugins
Plugins are the extension mechanism of jaato. They provide tools, garbage collection strategies, and model provider implementations.
Plugin Types
jaato has three categories of plugins:
| Type | Purpose | Examples |
|---|---|---|
| Tool | Provide capabilities the model can invoke | cli, mcp, file_edit, todo |
| GC | Manage context window limits | gc_truncate, gc_summarize |
| Provider | Connect to AI services | google_genai |
This page focuses on Tool Plugins, which are the most common type you'll work with.
from jaato import PluginRegistry
registry = PluginRegistry(model_name="claude-sonnet-4-20250514")
# Discover each type
tools = registry.discover(plugin_kind="tool")
gc = registry.discover(plugin_kind="gc")
providers = registry.discover(plugin_kind="model_provider")
print(f"Tool plugins: {tools}")
print(f"GC plugins: {gc}")
print(f"Providers: {providers}")
Registry vs Direct Configuration
Not all plugins are managed through the PluginRegistry.
There's an important distinction:
| Management | Plugin Types | How to Enable |
|---|---|---|
| Registry | Tool plugins | registry.expose_tool() |
| Client | GC, Session, Permission | client.set_*_plugin() |
| Constructor | Model Providers | JaatoClient(provider_name=) |
Why the Difference?
Tool plugins provide capabilities to the model and can be mixed and matched freely. GC, session, and permission plugins affect client behavior directly and require explicit configuration.
list_available(). They need to be created and
attached to the client directly.
# Tool plugins: discover → expose → configure
registry = PluginRegistry(model_name="claude-sonnet-4-20250514")
registry.discover()
registry.expose_tool("cli")
registry.expose_tool("file_edit")
client.configure_tools(registry)
# GC plugin: create → initialize → set
from shared.plugins.gc_truncate import create_plugin
gc = create_plugin()
gc.initialize({"preserve_recent_turns": 5})
client.set_gc_plugin(gc)
# Session plugin: create → initialize → set
from shared.plugins.session import create_plugin
session = create_plugin()
session.initialize({"storage_path": ".sessions"})
client.set_session_plugin(session)
# Permission plugin: passed to configure_tools
from shared import PermissionPlugin
perm = PermissionPlugin()
perm.initialize({"config_path": "perms.json"})
client.configure_tools(registry, permission_plugin=perm)
# Provider selected when creating client
client = JaatoClient(provider_name="google_genai")
# Cannot be changed after construction
# Create a new client for different provider
Plugin Discovery
Plugins are discovered via two mechanisms:
| Method | Use Case | Location |
|---|---|---|
| Entry Points | External packages, production | pyproject.toml |
| Directory | Built-ins, development | shared/plugins/ |
Entry Points (Recommended)
External packages register plugins via Python entry points in
pyproject.toml. This is the recommended approach for
distributing plugins.
Directory Scanning
For development and built-in plugins, the registry scans
shared/plugins/ for modules with a
create_plugin() factory function.
# In pyproject.toml of external package
[project.entry-points."jaato.plugins"]
my_plugin = "my_package.plugins:create_plugin"
# The entry point must reference a factory function
# that returns a ToolPlugin instance
from jaato import PluginRegistry
registry = PluginRegistry()
# Both entry points + directory (default)
registry.discover()
# Entry points only (installed packages)
registry.discover(include_directory=False)
# Check what was found
print(registry.list_available())
# ['cli', 'mcp', 'my_plugin', ...]
The Plugin Registry
The PluginRegistry is the central manager for tool plugins.
It handles discovery, registration, and exposure.
Key Concepts
| Term | Meaning |
|---|---|
| Discovered | Found and available for use |
| Registered | Loaded into the registry |
| Exposed | Tools are available to the model |
Discovery finds plugins, but they're not active until exposed. This gives you control over what capabilities the model has.
from jaato import PluginRegistry
registry = PluginRegistry(model_name="claude-sonnet-4-20250514")
# Step 1: Discover what's available
registry.discover()
print(f"Available: {registry.list_available()}")
# ['cli', 'mcp', 'file_edit', 'todo', ...]
# Step 2: Expose what you need
registry.expose_tool("cli")
registry.expose_tool("file_edit")
print(f"Exposed: {registry.list_exposed()}")
# ['cli', 'file_edit']
# Step 3: Check if specific plugin is exposed
if registry.is_exposed("cli"):
print("CLI tools are active")
# Step 4: Get tools for the client
schemas = registry.get_exposed_tool_schemas()
executors = registry.get_exposed_executors()
Built-in Tool Plugins
jaato includes several tool plugins out of the box:
cli
Execute shell commands. Supports auto-backgrounding for long-running commands.
mcp
Connect to Model Context Protocol servers. Auto-discovers tools from
MCP servers defined in .mcp.json.
file_edit
Read, write, and edit files. Supports diff-based editing for precise changes.
todo
Manage task lists. Persistent storage with filtering and status tracking.
web_search
Search the web. Returns structured search results.
permission
Control tool execution permissions. Approve/deny tool calls based on rules.
# Expose with default config
registry.expose_tool("cli")
registry.expose_tool("file_edit")
registry.expose_tool("todo")
# Expose with custom config
registry.expose_tool("cli", config={
"extra_paths": ["/opt/bin"],
"max_output_chars": 10000,
"auto_background_threshold": 5.0
})
# Expose MCP with specific servers
registry.expose_tool("mcp", config={
"servers": ["Atlassian", "GitHub"]
})
# Expose all at once
registry.expose_all()
# Expose all with per-plugin config
registry.expose_all(config={
"cli": {"max_output_chars": 5000},
"todo": {"storage_path": ".tasks.json"}
})
# List tools from exposed plugins
schemas = registry.get_exposed_tool_schemas()
for schema in schemas:
print(f"{schema.name}")
print(f" {schema.description}")
print()
# Example output:
# execute_command
# Execute a shell command
#
# read_file
# Read contents of a file
#
# edit_file
# Edit a file with diff
Tool Plugin Protocol
Tool plugins implement a simple protocol. At minimum, they provide:
-
name
str
Unique identifier for the plugin
-
get_tool_schemas()
List[ToolSchema]
Declare available tools
-
get_executors()
Dict[str, Callable]
Map tool names to functions
Optional Methods
-
initialize(config)
Setup with configuration
-
shutdown()
Cleanup resources
-
get_system_instructions()
Add to system prompt
-
get_auto_approved_tools()
Tools that don't need permission
-
get_model_requirements()
Required model patterns
from jaato import ToolSchema
class WeatherPlugin:
name = "weather"
def get_tool_schemas(self):
return [
ToolSchema(
name="get_weather",
description="Get weather for a city",
parameters={
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
)
]
def get_executors(self):
return {
"get_weather": self._get_weather
}
def _get_weather(self, city: str) -> str:
# Implementation here
return f"Weather in {city}: Sunny, 72°F"
class AdvancedPlugin:
name = "advanced"
def initialize(self, config):
self.api_key = config.get("api_key")
def shutdown(self):
# Cleanup connections
pass
def get_tool_schemas(self):
return [...]
def get_executors(self):
return {...}
def get_system_instructions(self):
return "Use advanced_tool for X..."
def get_auto_approved_tools(self):
return ["safe_tool"]
def get_model_requirements(self):
return ["gemini-*"] # Gemini only
Executor Metadata
Executors can return a (result_dict, metadata_dict) tuple
instead of a plain dict to pass UI hints to the presentation layer.
The ToolExecutor merges the metadata into the result
before forwarding it.
Metadata Keys
| Key | Type | Default | Description |
|---|---|---|---|
continuation_id |
str |
— | Groups tool calls into a single popup session |
show_output |
bool |
True |
Controls main output panel visibility |
show_popup |
bool |
True |
Controls popup panel tracking/visibility |
When metadata is not needed, return a plain dict as usual. The tuple form is only required when you need to communicate presentation hints.
def _exec_input(self, args):
session_id = args.get("session_id")
text = args.get("text", "")
session = self._sessions[session_id]
output = session.send_input(text)
result = {
"session_id": session_id,
"output": output,
}
if session.is_alive:
# Group into popup, hide from main panel
return (result, {
"continuation_id": session_id,
"show_output": False,
})
# Session ended — return plain result
return result
def _get_weather(self, args):
city = args.get("city")
# Plain dict — no metadata needed
return {"city": city, "temp": "72°F"}
Registering Custom Plugins
You can register custom plugins manually using
register_plugin().
Parameters
-
plugin
ToolPlugin
Your plugin instance
-
expose
bool
Expose immediately (default: False)
-
enrichment_only
bool
Only use for prompt enrichment
-
config
Dict
Plugin configuration
from my_plugins import WeatherPlugin
registry = PluginRegistry(model_name="claude-sonnet-4-20250514")
registry.discover() # Discover built-ins
# Register custom plugin
weather = WeatherPlugin()
registry.register_plugin(
weather,
expose=True,
config={"api_key": "..."}
)
# Now available alongside built-ins
print(registry.list_exposed())
# ['cli', 'weather']
# Plugin that modifies prompts but
# doesn't provide tools
class ContextPlugin:
name = "context"
def subscribes_to_prompt_enrichment(self):
return True
def enrich_prompt(self, prompt):
cwd = os.getcwd()
return PromptEnrichmentResult(
prompt=f"[CWD: {cwd}]\n{prompt}",
original_prompt=prompt,
enrichments=["added_cwd"]
)
def get_tool_schemas(self):
return [] # No tools
# Register for enrichment only
registry.register_plugin(
ContextPlugin(),
enrichment_only=True
)
System Instructions
Plugins can contribute to the system prompt via
get_system_instructions(). The registry combines
instructions from all exposed plugins.
Use system instructions to:
- Explain when to use your tools
- Provide usage guidelines
- Set behavioral constraints
class DatabasePlugin:
name = "database"
def get_system_instructions(self):
return """
When working with the database:
- Always use transactions for writes
- Prefer query_database over raw_sql
- Never expose connection strings
""".strip()
def get_tool_schemas(self):
return [
ToolSchema(
name="query_database",
description="Run a safe query",
parameters={...}
)
]
# Get all system instructions
instructions = registry.get_system_instructions()
if instructions:
print("Combined system prompt:")
print(instructions)
# The client uses this automatically
# when configuring tools
Model Tools vs User Commands
Plugins can provide two types of capabilities:
| Capability | Invoked By | Declared Via |
|---|---|---|
| Model Tools | AI model (function calling) | get_tool_schemas() |
| User Commands | User directly (bypasses model) | get_user_commands() |
When to Use Which?
- Model tools: When the AI should decide when to use it
- User commands: For direct control, admin tasks, or quick actions
class SearchPlugin:
name = "search"
def get_tool_schemas(self):
# MODEL TOOLS: AI decides when to use
return [
ToolSchema(
name="search_index",
description="Search the index",
parameters={...}
)
]
def get_user_commands(self):
# USER COMMANDS: User invokes directly
return [
UserCommand(
"search",
"Search directly",
share_with_model=True
),
UserCommand(
"reindex",
"Rebuild index",
share_with_model=False
)
]
Dual Exposure: Same Capability, Two Interfaces
Sometimes you want the same capability available both ways:
- As a model tool the AI can invoke
- As a user command for direct access
share_with_model
The share_with_model flag controls whether user command
output is added to conversation history:
True: Output goes to history, model can see/use it on subsequent turnsFalse: Output only shown to user, model never sees it
The share_with_model flag does not expose the command
as a tool. Tool exposure requires declaring the capability in get_tool_schemas().
These are independent declarations:
- A user command with
share_with_model=Trueis not automatically a tool - A tool is not automatically a user command
- For dual exposure, you must declare both explicitly
Think of share_with_model as: "when the user runs this command, should the
model know about it?" — not "should this also be a tool".
Auto-Approval
User commands typically should be auto-approved since the user
invokes them directly. List them in get_auto_approved_tools()
to avoid unexpected permission prompts.
get_auto_approved_tools(),
the permission system will prompt for approval — confusing since
the user explicitly requested it.
class TodoPlugin:
name = "todo"
def get_tool_schemas(self):
# Model can add/list todos
return [
ToolSchema(name="add_todo", ...),
ToolSchema(name="list_todos", ...),
]
def get_user_commands(self):
# User can also do it directly
return [
UserCommand(
"todo_add",
"Add a todo",
share_with_model=True # Model sees what was added
),
UserCommand(
"todo_list",
"List todos",
share_with_model=True # Model sees the list
),
UserCommand(
"todo_clear",
"Clear all todos",
share_with_model=False # Admin only
)
]
def get_auto_approved_tools(self):
# User commands don't need permission prompts
return ["todo_add", "todo_list", "todo_clear"]
# share_with_model=True:
# User: /todo_list
# → Shows list to user
# → Adds to conversation history
# → Model: "I see you have 3 pending todos..."
# share_with_model=False:
# User: /todo_clear
# → Clears todos
# → Shows "Cleared" to user
# → NOT in history, model doesn't know
UserCommand Reference
The UserCommand type declares a user-facing command.
Fields
-
name
str
Command name (e.g., "list_todos")
-
description
str
Help text for autocomplete
-
share_with_model
bool
Add output to conversation history (default: False)
-
parameters
List[CommandParameter]
Optional parameter definitions
CommandParameter
-
name
str
Parameter name
-
description
str
Help text
-
required
bool
Is this parameter required?
-
capture_rest
bool
Capture all remaining args (last param only)
from jaato import (
UserCommand,
CommandParameter
)
def get_user_commands(self):
return [
# Simple command, no parameters
UserCommand(
name="status",
description="Show status"
),
# Command with parameters
UserCommand(
name="search",
description="Search for pattern",
share_with_model=True,
parameters=[
CommandParameter(
name="pattern",
description="Search pattern",
required=True
),
CommandParameter(
name="description",
description="Optional notes",
capture_rest=True # Gets remaining args
)
]
)
]
# List available commands
commands = client.get_user_commands()
for name, cmd in commands.items():
print(f"/{name}: {cmd.description}")
# Execute a command
result, share = client.execute_user_command("status")
# With arguments
result, share = client.execute_user_command(
"search",
args={"pattern": "TODO", "description": "Find todos"}
)
Model Requirements
Some plugins only work with specific models. Use
get_model_requirements() to specify required models
using glob patterns.
If the current model doesn't match, the plugin is skipped during discovery.
Pattern Examples
| Pattern | Matches |
|---|---|
gemini-* |
All Gemini models |
gemini-2.5-* |
Gemini 2.5 family |
*-pro |
Any "pro" model |
claude-* |
Claude models |
class GeminiOnlyPlugin:
name = "gemini_features"
def get_model_requirements(self):
# Only works with Gemini
return ["gemini-*"]
def get_tool_schemas(self):
return [...]
# Check skipped plugins
registry = PluginRegistry(model_name="claude-3")
registry.discover()
skipped = registry.list_skipped_plugins()
for name, requirements in skipped.items():
print(f"{name} requires: {requirements}")
# gemini_features requires: ['gemini-*']
Next Steps
- Tools — How tool execution works
- Building Plugins — Step-by-step guide
- PluginRegistry API — Full reference
# Built-in plugins location:
# shared/plugins/
# ├── cli/
# │ ├── __init__.py
# │ └── plugin.py
# ├── mcp/
# │ ├── __init__.py
# │ └── plugin.py
# ├── file_edit/
# ├── todo/
# ├── web_search/
# ├── permission/
# ├── session/
# ├── gc_truncate/
# ├── gc_summarize/
# └── model_provider/
# ├── google_genai/
# └── types.py