Skip to content
GitHub

Guide

PackageRequiredPurpose
NoneZero-dependency protocol package

lexigram-contracts is the zero-dependency protocol layer at the base of the Lexigram ecosystem. It defines the interfaces — protocols, shared value types, base exceptions, and cross-package enums — that every Lexigram package depends on.

Without a shared contract layer, extension packages would import from each other directly, creating a tangled dependency graph:

lexigram-web ──► lexigram-sql ← cross-import = coupling

Every protocol that two or more packages need lives here, not in any extension. Packages depend on contracts, never on each other:

lexigram-web ──► lexigram-contracts ◄── lexigram-sql

Think of this package as the vocabulary of the framework. It defines the words (CacheBackendProtocol, Result, LLMClientProtocol) that every package uses to communicate. The implementations live elsewhere — here we only define what something does, not how.

┌───────────────────────────────────────────────────────────┐
│ lexigram-contracts │
│ │
│ Core: ContainerRegistrarProtocol, Result, Provider │
│ Data: DatabaseProviderProtocol, RepositoryProtocol │
│ AI: LLMClientProtocol, EmbeddingClientProtocol │
│ Cache: CacheBackendProtocol │
│ Events: EventBusProtocol, CommandBusProtocol │
│ Auth: TokenManagerProtocol, PasswordHasherProtocol │
│ ... │
│ │
│ Exceptions: LexigramError, DomainError, │
│ ContainerError, AIError, ... │
│ │
│ Value Types: ChatMessage, HealthCheckResult, │
│ DomainEvent, TokenUsage, ... │
└───────────────────────────────────────────────────────────┘

Protocols are defined as typing.Protocol classes with @runtime_checkable. They define service boundaries but contain no implementation code.

from typing import Protocol, runtime_checkable
@runtime_checkable
class CacheBackendProtocol(Protocol):
"""Interface for cache backends."""
async def get(self, key: str) -> bytes | None:
"""Retrieve a value. Returns None if not found."""
...
async def set(self, key: str, value: bytes, ttl: int) -> None:
"""Store a value with a TTL in seconds."""
...

All protocols live in lexigram.contracts.*, organized by domain (not by package name):

DomainFileKey Protocols
Corecore/di.pyContainerRegistrarProtocol, ContainerResolverProtocol, BootContainerProtocol
Datadata/__init__.pyDatabaseProviderProtocol, RepositoryProtocol, UnitOfWorkProtocol
Cacheinfra/cache.pyCacheBackendProtocol, CacheProviderProtocol
AI/LLMai/llm.pyLLMClientProtocol, EmbeddingClientProtocol, TokenCounterProtocol
AI/Agentsai/__init__.pyAgentProtocol, ToolProtocol, ToolRegistryProtocol
Authauth/__init__.pyTokenManagerProtocol, PasswordHasherProtocol, AuthorizerProtocol
Eventsevents/__init__.pyEventBusProtocol, CommandBusProtocol, EventHandlerProtocol
Workflowworkflow/__init__.pySagaProtocol, SagaManagerProtocol

Value types are dataclasses, frozen dataclasses, and enums that appear in protocol method signatures. They always live in contracts if used across packages.

from dataclasses import dataclass
from enum import Enum
class Role(str, Enum):
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
TOOL = "tool"
@dataclass(frozen=True)
class ChatMessage:
role: Role
content: str
name: str | None = None
@dataclass(frozen=True)
class HealthCheckResult:
component: str
status: HealthStatus
message: str | None = None

Base exceptions in contracts, leaf exceptions in extension packages:

LexigramError (contracts)
├── DomainError
├── ContainerError
├── ProviderError
├── AIError (contracts)
│ ├── LLMError ← leaf exceptions in lexigram-ai-llm
│ ├── RAGError ← leaf exceptions in lexigram-ai-rag
│ └── ...
├── AgentError
└── ...

Cross-package enums use class X(str, Enum):

EnumLocationUsed By
ProviderPrioritycore/provider.pyEvery package’s provider
ServiceScopecore/scopes.pyContainer registration
HealthStatuscore/health.pyHealth checks everywhere
Environmentcore/config.pyConfig system
CircuitStateresilience/enums.pyCircuit breaker

from __future__ import annotations
from typing import Protocol
from lexigram.contracts.core.di import ContainerRegistrarProtocol
from lexigram.contracts.infra.cache import CacheBackendProtocol
from lexigram.contracts.data import DatabaseProviderProtocol
class UserRepositoryProtocol(Protocol):
async def find(self, user_id: str) -> dict | None: ...

Pattern 1: Protocol-Based Service Injection

Section titled “Pattern 1: Protocol-Based Service Injection”
from __future__ import annotations
from lexigram.contracts.infra.cache import CacheBackendProtocol
from lexigram.contracts.data import DatabaseProviderProtocol
class UserService:
def __init__(
self,
db: DatabaseProviderProtocol,
cache: CacheBackendProtocol | None = None,
) -> None:
self.db = db
self.cache = cache
from __future__ import annotations
from lexigram.contracts.core.result import Result, Ok, Err
from lexigram.contracts.exceptions.domain import NotFoundError
async def find(self, user_id: str) -> Result[dict, NotFoundError]:
user = await self.repo.get(user_id)
if not user:
return Err(NotFoundError(f"User {user_id} not found"))
return Ok(user)

Pattern 3: Creating a Cross-Package Exception

Section titled “Pattern 3: Creating a Cross-Package Exception”

Extension packages extend contracts base exceptions:

lexigram-ai-llm/exceptions.py
from lexigram.contracts.ai.exceptions import LLMError
class LLMRateLimitError(LLMError):
"""Raised when the LLM provider rate-limits the request."""
class LLMModelNotFoundError(LLMError):
"""Raised when the requested model is unavailable."""

  • Protocols define what, not how — no implementation code in contracts
  • One definition per name — never redefine a protocol, type, or exception from contracts
  • Domain organization — protocols organized by domain (ai/, data/, cache/), not by package name
  • Separate types from protocolsprotocols.py for interfaces, types.py for dataclasses, errors.py for exceptions
  • Use @runtime_checkable for all protocols to enable isinstance() checks
  • Frozen dataclasses for value types that cross package boundaries
  • str, Enum for all enums — never bare string constants