Best Practices
Guidelines for writing robust, performant, and maintainable Codumentor plugins.
Error Handling
Exceptions in handlers are caught by the bus; they do not crash the agent. Use logging:
from codumentor.utils.logging import get_logger
logger = get_logger("my_plugin")
async def my_handler(self, agent, ctx, payload):
try:
# Your logic
except Exception as e:
logger.error(f"Handler failed: {e}")
return None # Fail gracefully
- Always return
Noneon failure so the pipeline continues. - Use structured logging with the plugin name for easy filtering.
- Avoid raising exceptions from hook handlers — they will be caught but may produce noisy logs.
Performance
- Keep handlers fast; expensive work should be async/background.
- Use
onResponsePersistfor non-blocking post-processing. - Implement summary/detail endpoints for UI widgets.
- Use
getBaseStyles()once per component, not per render.
Background Processing with Job Queue
For expensive operations (LLM calls, file processing), use the Job Queue System instead of direct async tasks:
from codumentor.core.job_queue import get_job_queue_registry, JobPriority
async def _on_response(self, agent, ctx, payload):
registry = get_job_queue_registry(self.config)
queue = registry.get_queue("background_llm")
await queue.enqueue(
job_type="my_job",
args={"data": payload["assistant_text"]}, # Must be JSON-serializable
priority=JobPriority.LOW,
)
Benefits:
- Persistence: Jobs survive server restarts
- Gate control: Respects system load (pauses when congested)
- UI events: Persist to event store for replay on reconnect
See Job Queue System for the full guide on registering job types, implementing gates, and handling recovered jobs.
Scheduled (recurring or one-off) work
When you need a job to run on a cron schedule — daily digests, periodic refresh, scheduled agent runs — use the Scheduler. It's a thin layer over the job queue; you register a regular job_type handler and the scheduler calls queue.enqueue(...) for you at the right times:
from codumentor.core.scheduler import get_scheduler
scheduler = get_scheduler(self.config, registry)
await scheduler.create(
owner_user_id=user_id,
job_type="daily_summary",
job_args={"repo": "team-repo"},
cron_expression="0 9 * * 1-5",
timezone="America/New_York",
)
See Scheduler for firing semantics (advance-then-enqueue, overlap skip, catch-up cutoffs) and per-user queue routing.
Configuration
Plugins are configured in codumentor.yaml:
plugins:
- module: "my_plugins.guardrail"
class: "GuardrailPlugin"
- module: "my_plugins.telemetry"
class: "TelemetryPlugin"
priority: 100 # Optional: override default priority
args:
log_level: "DEBUG"
include_metrics: true
Configuration Fields
| Field | Required | Description |
|---|---|---|
module | Yes | Python module path |
class | Yes | Plugin class name |
args | No | Arguments passed to constructor |
priority | No | Registration-priority override (lower = earlier) |
dependencies | No | List of plugin names that must register before this one (overrides manifest/class dependencies). See Plugin Load Order. |
Validating Configuration with Pydantic
Define a Pydantic model locally and build it from kwargs:
from pydantic import BaseModel
class MyPluginConfig(BaseModel):
enabled: bool = True
threshold: int = 100
class MyPlugin:
def __init__(self, config=None, **kwargs):
my_kwargs = {k: v for k, v in kwargs.items() if k in MyPluginConfig.model_fields}
self.plugin_config = MyPluginConfig(**my_kwargs)
Large Content & Chunking
Tool results that exceed the chunk limit (controlled by tools.tool_result_max_content_length, default 30 000 characters) are automatically stored in an in-memory ContentStore and truncated, with a continuation hint appended. The agent can then page through the rest using the read_content tool. No plugin code is required for this to work.
This happens transparently in Tool.call(), so any tool that returns a large ToolResult.content is handled.
When to use the manual API
If your tool has access to the full content before it builds the ToolResult (e.g., it pre-truncates internally), you can store the content explicitly for better control:
async def execute(self, arguments):
ctx = getattr(self, "_current_ctx", None) or {}
content_store = ctx.get("content_store")
full_text = extract_document(...) # e.g. 85K chars
if content_store:
from codumentor.utils.content_store import ContentStore
first_chunk, content_id, total_length, was_stored = (
content_store.store_if_long(
full_text,
metadata={"source": self.name, "file": path},
)
)
if was_stored:
hint = ContentStore.continuation_hint(
content_id, len(first_chunk), total_length,
)
return ToolResult(..., content=f"{header}\n{hint}\n\n{first_chunk}")
return ToolResult(..., content=f"{header}\n\n{full_text}")
This is useful when:
- The tool truncates internally and the full content isn't in the
ToolResult(e.g.,read_filefor documents) - You want per-source content IDs (e.g., one per URL in a multi-URL fetch)
Testing
Write unit tests using Python's unittest module:
import unittest
from my_plugin import MyPlugin
class TestMyPlugin(unittest.TestCase):
def test_handler(self):
plugin = MyPlugin()
# Test handler logic...
- Test hook handlers in isolation by calling them with mock
agent,ctx, andpayloadarguments. - Verify return values match the expected control types (
continue,replace, etc.). - Test edge cases: empty payloads, missing keys, error conditions.
UI Component Guidelines
- Always escape user content: Use
escapeHtml()for any dynamic text - Support dark mode: Use CSS custom properties, not hardcoded colors
- Use Shadow DOM: Provides style isolation from the host
- Handle all states: Render appropriately for
pending,success,error - Keep bundles small: The SDK is ~15KB; aim for <50KB total per plugin
Prompt Templates
If your plugin contributes a subagent or any other LLM-driven tool, do not hard-code the system prompt as a Python string. Codumentor ships a Jinja2-based prompt system that every core agent and every first-party subagent already uses. Reusing it means users can override your prompt via --prompt-dir without forking your plugin.
from codumentor.utils.template import get_template_loader
loader = get_template_loader(self.full_config.prompt.template_dir)
system_prompt = loader.render_template(
"my_plugin_system_prompt.jinja2",
{"query": user_query, "repo_name": repo.name},
)
Drop the .jinja2 file into the repo's prompts/ directory (same place as agentic_developer_system_prompt.jinja2 and friends), or ship it alongside your plugin and document the filename so users can override it. {% include "general_instructions.jinja2" %} at the bottom of a subagent prompt will pull in Codumentor's shared tool-calling and environment rules for free.
See the Prompt System reference for the full file catalog, available variables, and override mechanics.
Example Plugins
These existing plugins serve as useful references:
- Telemetry Plugin (catalog) — Multiple hook subscriptions, context data storage (
ctx.kv), report generation (onReportCollect), UI component with expandable details - Agentic Memory Plugin (catalog) — Multiple custom elements (card + widget), API routes for voting, welcome page widget with auto-refresh, two-tier data loading (summary + full)
- Template Plugin (
ui/plugins/_template/) — Minimal working example with SDK usage patterns, expandable card, and proper TypeScript setup
See Also
- Plugin Catalog — Available plugins and configuration
- Job Queue System — Background processing with persistence and gate control
- Scheduler — Recurring and one-off jobs on top of the job queue
- Examples — Working example plugins
- Web UI — Web UI integration details