Skip to content

Loom Coding, Documentation & Style Guide

This guide defines the coding, commenting, and documentation standards for all Loom contributors. Code that does not conform will be flagged during review or by the automated linter.

Read CONTRIBUTING.md first for architectural invariants and the CLA. This guide covers the how of writing code; CONTRIBUTING.md covers the what is acceptable.


Table of Contents

  1. Python Version and Language Features
  2. Code Formatting
  3. Naming Conventions
  4. Import Style
  5. Type Annotations
  6. Module Docstrings
  7. Class Docstrings
  8. Function and Method Docstrings
  9. Inline Comments
  10. Error Handling
  11. Logging
  12. Testing
  13. YAML Configuration
  14. Git Workflow
  15. Reference Examples

Python Version and Language Features

  • Python 3.11+ is required. Use modern syntax freely:
  • X | Y union syntax (not Union[X, Y])
  • dict[str, Any] lowercase generics (not Dict[str, Any])
  • from __future__ import annotations at the top of every module (PEP 563 deferred evaluation — keeps runtime import cost low and avoids forward-ref issues)
  • Pydantic v2 for all data models.
  • asyncio for all I/O-bound code. Actors are async; blocking calls must be offloaded to a thread pool (see SyncProcessingBackend).

Code Formatting

All formatting is enforced by ruff (configured in pyproject.toml).

Rule Setting
Line length 100 characters
Indentation 4 spaces (no tabs)
Quotes Double quotes (") preferred
Trailing commas Required in multi-line constructs
Blank lines 2 between top-level definitions, 1 within classes

Run the formatter before committing:

uv run ruff format src/ tests/
uv run ruff check src/ tests/ --fix

Naming Conventions

Element Convention Example
Modules snake_case nats_adapter.py
Classes PascalCase PipelineOrchestrator
Functions / methods snake_case resolve_tier()
Constants UPPER_SNAKE_CASE DEAD_LETTER_SUBJECT
Private members Leading underscore _running, _refill()
Type variables PascalCase + T suffix MessageT
Pydantic fields snake_case worker_type, goal_id
NATS subjects dot.separated.lowercase loom.tasks.incoming
CLI commands kebab-case (Click default) loom workshop
Config keys (YAML) snake_case max_concurrent_goals

Abbreviations: Avoid unless universally understood (url, id, db). Spell out domain terms (orchestrator not orch, message not msg — except in structlog event names where brevity is expected).


Import Style

Imports are organized into four groups separated by blank lines, sorted alphabetically within each group:

# 1. __future__ imports (always first)
from __future__ import annotations

# 2. Standard library
import asyncio
import json
from typing import Any

# 3. Third-party
import structlog
import yaml
from pydantic import BaseModel, Field

# 4. Local (loom package)
from loom.core.actor import BaseActor
from loom.core.messages import TaskMessage, TaskResult

Rules:

  • Use from X import Y for specific names; use import X for namespaces you reference multiple times (e.g., import json then json.loads()).
  • Never use wildcard imports (from X import *).
  • Conditional imports (for optional dependencies) go at point of use, not at module top:
# Good — only imported when needed, avoids hard dep
async def process(self, ...):
    from loom.worker.knowledge import load_knowledge_silos
    ...
  • Ruff enforces import sorting automatically via isort rules.

Type Annotations

  • Annotate all public function signatures (parameters and return types).
  • Private helper functions: annotations recommended but not strictly required.
  • Use Any sparingly — prefer specific types. dict[str, Any] is acceptable for JSON-like data flowing across actor boundaries.
  • Use | None instead of Optional[X].
  • For callback types, use collections.abc.Callable (not typing.Callable).
# Good
async def call_worker(
    self,
    worker_type: str,
    payload: dict[str, Any],
    tier: str = "standard",
    timeout: float = 60.0,
) -> dict[str, Any]:
    ...

# Bad — missing return type, uses old-style Optional
def call_worker(self, worker_type, payload, tier="standard", timeout=60.0):
    ...

Module Docstrings

Every .py file must have a module-level docstring immediately after the """ opening. This is the single most important piece of documentation — it tells a reader what this file does and why it exists without reading any code.

Required elements:

  1. One-line summary — what the module does.
  2. Context paragraph — where this fits in the architecture, what depends on it, and what it depends on.
  3. Design notes (if applicable) — why a particular approach was chosen, any invariants maintained, known limitations.
  4. See also (if applicable) — related modules for navigation.

Template:

"""
One-line summary of what this module does.

Longer description providing architectural context: how this module fits into
Loom, what calls it, what it calls. Explain the core abstraction or pattern.

Design note: why this approach was chosen over alternatives. For example,
why we use our own JSON Schema validator instead of the jsonschema library.

See also:
    loom.core.messages — the message types this module processes
    loom.bus.nats_adapter — the production bus implementation
"""

Good example (from core/actor.py):

"""
Base actor class — the foundation of Loom's actor model.

All Loom actors (workers, orchestrators, routers) inherit from BaseActor.
This class handles the message bus subscription lifecycle, message dispatch,
signal-based shutdown, and error isolation. Each actor is an independent
process with no shared memory.

Design invariant: actors communicate ONLY through bus messages (see messages.py).
Direct method calls between actors are forbidden.

The message bus is pluggable via the ``bus`` constructor parameter. The default
is NATSBus (created from ``nats_url`` when no bus is provided). For testing,
pass an InMemoryBus instead.
"""

Class Docstrings

Every public class must have a docstring explaining:

  1. What it is (one-line summary).
  2. How to use it — constructor parameters, key methods, expected lifecycle.
  3. Invariants — what guarantees it maintains (e.g., "stateless between tasks", "thread-safe", "not safe for concurrent use").

Use reStructuredText-style cross-references for related classes:

class PipelineOrchestrator(BaseActor):
    """
    Pipeline orchestrator with automatic stage parallelism.

    Processes an OrchestratorGoal by running it through a series of stages
    organized into execution levels based on their dependencies. Stages
    within the same level run concurrently; levels execute sequentially.
    Stage outputs are accumulated in a context dict and can be referenced
    by subsequent stages via input_mapping.
    """

Private/internal classes: A brief one-line docstring is sufficient.


Function and Method Docstrings

When to write a docstring

Visibility Rule
Public API (no underscore) Always — full docstring
Protected (_single_underscore) Required if non-trivial (>10 lines or complex logic)
Private (__double_underscore) Optional — brief comment often suffices
Dunder methods (__init__, __aiter__) Required if they accept non-obvious parameters

Docstring format

Use Google-style docstrings (compatible with Sphinx napoleon extension):

async def call_worker(
    self,
    worker_type: str,
    payload: dict[str, Any],
    tier: str = "standard",
    timeout: float = 60.0,
) -> dict[str, Any]:
    """Dispatch a task to a worker and wait for the result.

    Publishes a TaskMessage to loom.tasks.incoming and subscribes to
    the result subject. Blocks until a matching TaskResult arrives or
    the timeout expires.

    Args:
        worker_type: Which worker config to dispatch to (e.g., "summarizer").
        payload: Structured input conforming to the worker's input_schema.
        tier: Model tier override. Defaults to "standard".
        timeout: Maximum seconds to wait for a result.

    Returns:
        The worker's output dict (the ``output`` field of TaskResult).

    Raises:
        BridgeTimeoutError: If no result arrives within ``timeout`` seconds.
        BridgeError: If the worker returns a FAILED status.
    """

Rules

  • First line is a concise imperative summary ("Dispatch a task", not "Dispatches a task" or "This method dispatches a task").
  • Blank line between summary and body.
  • Args section: list every parameter (except self/cls). Include types only if they add clarity beyond the annotation.
  • Returns section: describe the return value structure. For dicts, mention key fields.
  • Raises section: list exceptions the caller should handle. Omit generic exceptions that indicate bugs (e.g., TypeError).
  • Keep docstrings accurate — an outdated docstring is worse than none. If you change a function's behavior, update the docstring in the same commit.

Inline Comments

When to comment

  • Why, not what. Don't restate the code. Explain the reasoning behind a non-obvious choice.
  • Gotchas and edge cases — especially Python quirks (e.g., bool is a subclass of int).
  • TODO markers — use # TODO: Strategy X — ... for planned future work. Reference the strategy letter from CLAUDE.md so the item is traceable.
  • Performance notes — if code is written a certain way for performance, say so.

Style

# Good — explains WHY
# Reject bools masquerading as ints (bool is a subclass of int)
if isinstance(value, bool) or not isinstance(value, int):

# Bad — restates the code
# Check if the value is a bool or not an int
if isinstance(value, bool) or not isinstance(value, int):
# Good — marks a design decision
# Sequential processing — strict mailbox semantics
await self._process_one(data)

# Good — TODO with strategy reference
# TODO: Strategy A — streaming result collection

Section headers

For long methods or complex logic, use section comment headers:

# ------------------------------------------------------------------
# Dependency inference and execution level construction
# ------------------------------------------------------------------

Keep these consistent: 70-char dashes, no blank line before the first line after the header.


Error Handling

  • Raise specific exceptions, not generic Exception or RuntimeError. Define custom exception classes for each module's failure modes:
class PipelineStageError(Exception):
    """Raised when a pipeline stage fails or times out."""

    def __init__(self, stage_name: str, message: str):
        self.stage_name = stage_name
        super().__init__(message)
  • Don't silence exceptions without logging:
# Good
except Exception as e:
    logger.error("actor.error", actor_id=self.actor_id, error=str(e))

# Bad
except Exception:
    pass
  • Actor isolation: individual message failures must not crash the actor loop. Catch at the message handler level and log.
  • Validate at boundaries: check inputs at actor/API boundaries (contract validation, message parsing), trust internal code.

Logging

Use structlog everywhere. Never use print() for operational output.

Event naming convention

{component}.{action}

Examples: actor.connected, router.dead_letter, worker.tool_round, pipeline.stage_completed.

Log levels

Level Use for
debug Internal state details (message contents, intermediate values)
info Normal operational events (connected, subscribed, task routed)
warning Recoverable issues (unknown tool, condition parse failure, rate limit)
error Failures that affect the current operation (task failed, backend error)

Structured fields

Always pass context as keyword arguments, not interpolated strings:

# Good
logger.info("router.routing", task_id=task.task_id, tier=tier.value)

# Bad
logger.info(f"Routing task {task.task_id} to tier {tier.value}")

Testing

File naming

  • Test file: test_{module_name}.py (mirrors src/loom/{package}/{module}.py)
  • Test class: Test{ClassName} (e.g., TestPipelineOrchestrator)
  • Test function: test_{behavior_description} (e.g., test_stage_timeout_produces_failed_result)

Test organization

"""Tests for loom.orchestrator.pipeline — PipelineOrchestrator."""
import pytest

from loom.orchestrator.pipeline import PipelineOrchestrator, PipelineStageError


class TestBuildExecutionLevels:
    """Execution level construction from dependency graphs."""

    def test_independent_stages_in_single_level(self):
        ...

    def test_circular_dependency_raises(self):
        ...


class TestExecuteStage:
    """Single-stage execution with mocked bus."""

    @pytest.fixture
    def pipeline(self):
        ...

Rules

  • No infrastructure required for unit tests. Use InMemoryBus and InMemoryCheckpointStore.
  • Mark integration tests with @pytest.mark.integration.
  • Every new feature must include unit tests. PRs without tests for new code will not be merged.
  • Test the contract, not the implementation. If you test internal methods directly, your tests are coupled to implementation details.
  • Use pytest.fixture for shared setup. Keep fixtures close to where they're used (same file or conftest.py).
  • asyncio_mode = "auto" is configured — async test functions work without the @pytest.mark.asyncio decorator.

Coverage

  • Minimum threshold: 70% (enforced in CI via fail_under).
  • Target: 85%+ for core modules (core/, worker/, orchestrator/, router/, bus/).
  • Use # pragma: no cover only for truly unreachable code (e.g., if __name__ == "__main__" guards, TYPE_CHECKING blocks).

YAML Configuration

Worker and pipeline configs live in configs/. Follow these conventions:

  • Top-level keys in snake_case.
  • Include a comment header explaining what the config does:
# summarizer.yaml — Summarize text into structured output.
# Tier: local (Ollama). See _template.yaml for all available keys.
name: summarizer
worker_type: summarizer
  • Schema fields (input_schema, output_schema) must be valid JSON Schema (type: object, with properties and required arrays).
  • Keep configs narrow — one responsibility per worker. Don't combine summarization and classification in a single config.

Git Workflow

  • Branch naming: feature/description, fix/description, docs/description.
  • Commit messages: imperative mood, concise summary line (<72 chars), with optional body explaining why:
Add dependency inference to PipelineOrchestrator

Stages now auto-infer dependencies from input_mapping paths instead of
requiring explicit depends_on lists. Uses Kahn's algorithm for
topological sort to detect cycles at config load time.
  • One logical change per commit. Don't mix refactors with features.
  • Run uv run ruff check src/ && uv run pytest tests/ -v -m "not integration" before pushing.

Reference Examples

The following source files exemplify these standards and should be used as references when writing new code:

Pattern Reference file
Module docstring with architecture context src/loom/orchestrator/pipeline.py
Class with lifecycle documentation src/loom/core/actor.py
Function with Args/Returns/Raises src/loom/worker/runner.py (execute_with_tools)
Custom exception hierarchy src/loom/orchestrator/pipeline.py
ABC with usage examples in docstring src/loom/bus/base.py
Pydantic models with field documentation src/loom/core/messages.py
Section headers in long classes src/loom/orchestrator/pipeline.py
Structured logging conventions src/loom/router/router.py
Contract validation with design rationale src/loom/core/contracts.py
Contrib backend with config docs src/loom/contrib/rag/backends.py