Codumentor logo Codumentor

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

Performance


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:

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

FieldRequiredDescription
moduleYesPython module path
classYesPlugin class name
argsNoArguments passed to constructor
priorityNoRegistration-priority override (lower = earlier)
dependenciesNoList 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:


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...

UI Component Guidelines

  1. Always escape user content: Use escapeHtml() for any dynamic text
  2. Support dark mode: Use CSS custom properties, not hardcoded colors
  3. Use Shadow DOM: Provides style isolation from the host
  4. Handle all states: Render appropriately for pending, success, error
  5. 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:


See Also