Skip to content
GitHub

Architecture

Internal design of the lexigram-contracts package.


lexigram-contracts is the zero-dependency foundation layer of the Lexigram ecosystem. Every protocol, shared value type, base exception, and cross-package enum lives here.

flowchart BT
    Ext[Extension packages<br/>lexigram-web · lexigram-sql · lexigram-ai-llm<br/>lexigram-ai-rag · lexigram-admin · lexigram-cache]
    Core[lexigram<br/>Core framework · DI · Config · IoC]
    Contracts[lexigram-contracts<br/>Protocols · Types · Exceptions · Enums]

    Ext --> Core
    Core --> Contracts
    Ext -.->|imports only| Contracts

Zero-dependency rule: only Python stdlib — no ecosystem imports.

from typing import Protocol # ✅ stdlib
from dataclasses import dataclass
from enum import Enum
# from pydantic import BaseModel # ❌ transitive deps on every consumer

If two or more packages need to reference the same type, protocol, or exception, it lives in lexigram-contracts. No exceptions.

flowchart TD
    Q{Shared across<br/>2+ packages?}
    Q -->|Yes| Contracts[lexigram-contracts]
    Q -->|No| Q2{Type used in<br/>protocol signature?}
    Q2 -->|Yes| Contracts
    Q2 -->|No| Q3{Caller needs<br/>to catch it?}
    Q3 -->|Yes| Contracts[Base in contracts<br/>Leaves in extension]
    Q3 -->|No| Q4{Cross-package enum?}
    Q4 -->|Yes| Contracts
    Q4 -->|No| Extension[Keep in extension package]

A type crossing extension boundaries must be moved to contracts.


Protocols, types, and exceptions are organized by domain, not by package name. Each domain directory follows a consistent structure:

FilePurpose
protocols.pyProtocol definitions (interfaces)
types.pyShared value types (dataclasses, type aliases)
errors.pyBase exception classes
__init__.pyRe-exports for ergonomic importing
DomainKey Contents
core/Result[T,E], ContainerProtocol, ProviderProtocol, HealthCheckResult, ClockProtocol, ConfigProtocol, ServiceScope, Lifecycle
ai/LLMClientProtocol, ChatMessage, Role, TokenUsage, MemoryStoreProtocol, RAGPipelineProtocol, AIError hierarchy, SkillProtocol, EmbeddingClientProtocol
data/DatabaseProviderProtocol, RepositoryProtocol, VectorStoreProtocol, UnitOfWorkProtocol, SQLDialect
exceptions/LexigramError, DomainError, InfrastructureError, ContainerError, SecurityError, ResilienceError, ProviderError
domain/AggregateRootProtocol, DomainEvent, SpecificationProtocol, CursorPage
events/EventBusProtocol, CommandBusProtocol, EventHandlerProtocol, EventStoreProtocol
auth/TokenManagerProtocol, PasswordHasherProtocol, AuthorizerProtocol
infra/CacheBackendProtocol, TaskQueueProtocol, BlobStoreProtocol, CircuitBreakerConfig
security/HasherProtocol, KeyDerivationProtocol, SecretStoreProtocol
tenancy/TenantResolverProtocol, TenantInfo, TenantStatus
web/CORSPolicyProtocol, ErrorDetail, ErrorResponseDTO
mcp/MCPServerProtocol, MCPTransportProtocol, MCPError
workflow/SagaProtocol, SagaManagerProtocol, WorkflowNodeProtocol
notification/, queue/, webhook/, cli/, feature_flags/, lifecycle/, observability/, mapping/, search/, graphql/, mailer/, codegen/, admin/, lib/Smaller domains, each with protocols and types

Protocols define service boundaries. Their placement determines the entire dependency graph.

flowchart TD
    Q1{Consumed by >1 package?}
    Q1 -->|Yes| C[lexigram-contracts]
    Q1 -->|No| Q2{Container-registered service contract?}
    Q2 -->|Yes| C
    Q2 -->|No| Q3{Pluggable backend?}
    Q3 -->|Yes| C
    Q3 -->|No| Q4{Strictly internal to one package?}
    Q4 -->|Yes| E[Extension package]
    Q4 -->|Unsure| C

Prohibited: same protocol in two files (import collision), extension protocol consumed by another extension (cross-import), protocol with implementation (interfaces only), protocols.py containing dataclasses (separate types).


Value types appear in function signatures framework-wide.

TypeLocationConsumed By
ChatMessage, Role, Completion, TokenUsageai/llm.pyllm, agents, rag, memory, prompt
Document, SearchResultdata/vector/rag, vector, memory
MemoryEntry, MemoryQueryai/memory.pymemory, agents
AgentResponseai/types.pyagents, ai
SkillDefinition, SkillResultai/skills.pyskills, agents
ModelRequest, ModelResponseai/models.pyllm, rag, routing
HealthCheckResultcore/health.pyevery provider
DomainEventdomain/events.pyevents, all aggregates
Result[T, E], Ok, Errcore/result.pyevery package
TenantInfo, TenantStatustenancy/every multi-tenant package
TypeLocationReason
ConversationStatslexigram-ai-llmLLM conversation management only
PlanSteplexigram-ai-agentsPlan-and-execute internal
Chunk, PipelineContextlexigram-ai-ragRAG implementation internals
CacheEntry, CacheStatslexigram-cacheCache internals
Package config classesEach packagePackage-specific configuration

Two-level hierarchy: base exceptions in contracts, leaf exceptions in extension packages.

flowchart LR
    subgraph C[lexigram-contracts]
        LE[LexigramError]
        LE --> DE[DomainError]
        LE --> IE[InfrastructureError]
        LE --> CE[ContainerError]
        LE --> SE[SecurityError]
        LE --> RE[ResilienceError]
        LE --> AE[AIError → LLMError · RAGError · MemoryError · SkillError · GuardError]
    end
    subgraph E[Extension Packages]
        AE -.->|extended by| L1[LLMRateLimitError · LLMModelNotFoundError]
        AE -.->|extended by| L2[RetrievalError · SynthesisError]
        AE -.->|extended by| L3[MemoryStoreError · ConsolidationError]
        AE -.->|extended by| L4[AgentExecutionError · ToolNotFoundError]
    end
# Catch in contracts — no extension import needed
from lexigram.contracts.ai import AIError
try:
result = await llm.generate(prompt)
except AIError as e:
logger.warning("llm_failed", error=str(e))
# Specific leaf — import the extension
from lexigram_ai_llm.exceptions import LLMRateLimitError
RuleRationale
Base exceptions in contractsCallers catch at domain boundary without importing extensions
Leaf exceptions in extension packagesSpecific to one implementation
No exception defined in two placesOne definition, one import path

Every enumeration uses class X(str, Enum): — never bare string constants.

class Role(str, Enum): # ✅
SYSTEM = 'system'
class Role: # ❌ not a real enum
SYSTEM = 'system'
EnumLocationUsed By
Roleai/llm.pyllm, agents, rag, memory, prompt
ServiceScopecore/scopes.pyevery DI-capable package
ProviderPrioritycore/provider.pyevery provider
HealthStatuscore/health.pyevery package
Environmentcore/config.pyevery package
SQLDialectdata/sql/db, search
TenantStatustenancy/multi-tenant extensions
JobStatusinfra/tasks.pytask systems
FlagTypefeature_flags/feature flag consumers

Every protocol, type, exception, and enum has exactly ONE definition in the entire monorepo.

If a name exists in lexigram-contracts, no extension package may redefine it. Re-export aliases (ai/vector.pydata/vector/protocols.py) are permitted for ergonomic imports — simple re-exports only, no added logic.


Canonical source tree layout:

lexigram-contracts/src/lexigram/contracts/
├── __init__.py # Lazy imports, all public API
├── core/ # DI, Result, provider, health, clock, identity, locks
│ ├── constants.py # ENV_PREFIX, __version__, EP_* discovery groups
│ ├── config.py # ConfigProtocol, Environment
│ ├── di.py # ContainerRegistrarProtocol, ContainerResolverProtocol
│ ├── result.py # Result[T,E], Ok, Err
│ ├── provider.py # ProviderPriority, ProviderProtocol
│ ├── health.py # HealthCheckResult, AggregateHealthResult, HealthStatus
│ ├── clock.py # ClockProtocol
│ ├── identity.py # IdGeneratorProtocol, IdStrategy
│ ├── lock.py # AsyncLockProtocol, DistributedLockProtocol
│ ├── scopes.py # ServiceScope
│ └── ...
├── ai/ # LLM, memory, RAG, skills, guards, routing, models
│ ├── exceptions.py # AIError, LLMError, RAGError, MemoryError, SkillError (bases)
│ ├── llm.py # ChatMessage, Role, Completion, TokenUsage, LLMClientProtocol
│ ├── memory.py # MemoryStoreProtocol, MemoryEntry, MemoryQuery
│ ├── skills.py # SkillProtocol, SkillDefinition, SkillResult
│ ├── rag.py # RAGPipelineProtocol, ChunkProtocol
│ ├── guards.py # GuardPipelineProtocol, InputGuardProtocol
│ ├── vector.py # Re-exports from data/vector/
│ └── models.py # ModelRequest, ModelResponse
├── data/ # Database protocols, vector store, SQL, repositories
│ ├── protocols.py # DatabaseProviderProtocol, RepositoryProtocol
│ ├── vector/protocols.py # VectorStoreProtocol, VectorCollectionProtocol
│ ├── vector/types.py # SearchResult
│ └── sql/sql_dialect.py # SQLDialect enum
├── exceptions/ # Full exception hierarchy
│ ├── base.py # LexigramError
│ ├── domain.py # DomainError, NotFoundError, ValidationError
│ ├── infra.py # InfrastructureError, DatabaseError
│ ├── container.py # ContainerError, CircularDependencyError
│ ├── provider.py # ProviderError, ModuleError
│ ├── security.py # SecurityError, GuardDeniedError
│ ├── resilience.py # ResilienceError, CircuitBreakerError
│ └── events.py # EventError, HandlerNotFoundError
├── domain/ # Aggregates, events, pagination, specification
├── events/ # EventBus, CommandBus, EventHandler protocols
├── auth/ # TokenManager, PasswordHasher, Authorizer protocols
├── security/ # Hasher, KeyDerivation, SecretStore protocols
├── tenancy/ # TenantResolver, TenantInfo, TenantStatus, tenant errors
├── web/ # CORS, ErrorDetail, ErrorResponseDTO
├── infra/ # Cache, tasks, storage, state, resilience configs
├── mcp/ # MCP server/transport protocols, MCPError
├── workflow/ # Saga, SagaManager, WorkflowNode protocols
└── (notification, queue, webhook, feature_flags, cli,
observability, lifecycle, mapping, search, graphql, mailer,
codegen, admin, lib) # Smaller domain subdirectories, each with protocols + types

No runtime lifecycle. Protocols and types are defined at import time.


  • New protocol → add protocols.py under the domain directory
  • New shared type → add dataclass/enum to domain’s types.py
  • New base exception → add to domain’s errors.py
  • Re-export path → simple alias for ergonomic imports
  • New domain → new top-level directory for an emerging subsystem
ItemContracts?
Protocol used by 2+ packagesYes
Dataclass in a protocol signatureYes
Base exception for callers to catchYes (leaf → extension)
Cross-package enumYes
Pluggable backend protocolYes
Internal helper classNo