Architecture & Code Guide
The Ainalyn SDK is built on Hexagonal Architecture (Ports & Adapters), ensuring clean separation of concerns, testability, and maintainability.
Note: This section is for contributors who want to understand or modify the SDK’s internal architecture. If you’re using the SDK to build Agents, see the Concepts section instead.
Core Principles
- Compiler, Not Runtime - The SDK creates Agent Definitions; execution belongs to the platform
- Dependency Inversion - Core business logic depends on abstractions, not implementations
- Clean Boundaries - Each component has a single, well-defined responsibility
- Immutability - Domain entities are immutable by design
Architecture Diagram
┌─────────────────────┐
│ CLI (cli.py) │
│ Inbound Adapter │
└──────────┬──────────┘
│
┌──────────────────┐ │ ┌──────────────────┐
│ Python API │ │ │ YAML Serializer │
│ (api.py) │ │ │ Outbound Adapter│
│ Inbound Adapter │ │ └────────┬─────────┘
└────────┬─────────┘ │ │
│ │ │
│ ╔═════════════════╧══════════════════╗ │
│ ║ ║ │
└────────▶║ APPLICATION CORE ║◀──────┘
║ ┌──────────────────────────────┐ ║
┌──────────────┐ ║ │ Application │ ║ ┌──────────────┐
│ Builders │ ║ │ ┌────────────────────────┐ │ ║ │ Schema │
│ (inbound/ │──▶║ │ │ Services & Use Cases │ │ ║◀──│ Validator │
│ builders/) │ ║ │ │ (Orchestration) │ │ ║ │ (outbound/)│
└──────────────┘ ║ │ └───────────┬────────────┘ │ ║ └──────────────┘
║ │ │ │ ║
║ │ Ports │ Ports │ ║
║ │ (inbound) │ (outbound) │ ║
║ │ │ │ ║
║ │ ┌───────────▼────────────┐ │ ║ ┌──────────────┐
║ │ │ Domain │ │ ║ │ Static │
║ │ │ ┌──────────────────┐ │ │ ║◀──│ Analyzer │
║ │ │ │ Entities, Rules │ │ │ ║ │ (outbound/)│
║ │ │ │ (Business Logic) │ │ │ ║ └──────────────┘
║ │ │ └──────────────────┘ │ │ ║
║ │ └────────────────────────┘ │ ║
║ └──────────────────────────────┘ ║
║ ║
╚════════════════════════════════════╝
│
┌──────────┴──────────┐
│ Infrastructure │
│ (service_factory) │
│ Wiring/DI │
└─────────────────────┘Unlike layered/onion architecture where layers wrap around each other, hexagonal architecture places the Application Core at the center with Ports defining boundaries and Adapters connecting to the outside world.
Key Concepts
| Concept | Description | Location |
|---|---|---|
| Application Core | Business logic that never depends on external systems | domain/ + application/ |
| Inbound Ports | Interfaces for use cases (what the core offers) | application/ports/inbound/ |
| Outbound Ports | Interfaces for external capabilities the core needs | application/ports/outbound/ |
| Inbound Adapters | Entry points that drive the application (CLI, API, Builders) | adapters/inbound/, api.py, cli.py |
| Outbound Adapters | Implementations of external capabilities (validators, serializers) | adapters/outbound/ |
| Infrastructure | Dependency injection and wiring | infrastructure/ |
Dependency Rules
Dependencies always point inward toward the Application Core:
Inbound Adapters
(api.py, cli.py, builders/)
│
│ calls
▼
┌───────────────────────┐
│ Application Layer │
│ (services, use_cases)│
│ │ │
│ defines ports │
│ │ │
└───────────┼───────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
Outbound Ports Domain Layer Infrastructure
(interfaces) (entities, rules) (wiring/DI)
▲ │
│ │
└──────────────────────────────────────────────┘
implements ports with
Outbound Adapters
(schema_validator, yaml_serializer)| Component | Can Depend On | Cannot Depend On |
|---|---|---|
| Domain | Nothing (pure Python) | Any outer layer |
| Application (ports) | Domain | Adapters, Infrastructure |
| Application (services) | Domain, Ports (interfaces) | Concrete Adapters |
| Inbound Adapters | Application (via services), Domain | Outbound Adapters |
| Outbound Adapters | Domain, implements Outbound Ports | Application internals |
| Infrastructure | Everything (for wiring) | - |
Layer Details
Domain Layer
Location: ainalyn/domain/
Pure business logic with zero external dependencies.
@dataclass(frozen=True)
class AgentDefinition:
"""Immutable domain entity."""
name: str
version: str
workflows: tuple[Workflow, ...]
def __post_init__(self) -> None:
if not DefinitionRules.is_valid_name(self.name):
raise InvalidFormatError(f"Invalid name: {self.name}")Domain Errors:
class DefinitionError(Exception):
"""Base error for Agent Definition issues."""
class InvalidFormatError(DefinitionError):
"""Field format validation error."""
class CyclicDependencyError(DefinitionError):
"""Workflow contains cycles."""Application Layer
Location: ainalyn/application/
Orchestrates domain logic via port interfaces.
Ports - Define contracts:
class IDefinitionSchemaValidator(Protocol):
"""Port for schema validation capability."""
def validate_schema(self, definition: AgentDefinition) -> tuple[ValidationError, ...]: ...Services - Orchestrate via dependency injection:
class DefinitionService:
def __init__(
self,
schema_validator: IDefinitionSchemaValidator, # Port, not concrete
static_analyzer: IDefinitionAnalyzer,
serializer: IDefinitionSerializer,
):
self._schema_validator = schema_validator
# ...Adapters Layer
Location: ainalyn/adapters/
Inbound - Entry points (Builders):
class AgentBuilder:
"""Fluent builder for Agent Definitions."""
def version(self, version: str) -> AgentBuilder:
self._version = version
return self
def build(self) -> AgentDefinition:
return AgentDefinition(name=self._name, ...)Outbound - External capabilities:
class YamlSerializer:
"""Implements IDefinitionSerializer port."""
def serialize(self, definition: AgentDefinition) -> str:
return yaml.dump(self._to_dict(definition))Infrastructure Layer
Location: ainalyn/infrastructure/
Wires concrete adapters to ports:
def create_default_service() -> DefinitionService:
return DefinitionService(
schema_validator=SchemaValidator(), # Concrete adapter
static_analyzer=StaticAnalyzer(),
serializer=YamlSerializer(),
)Related Pages
- System Context - SDK’s role in the overall system
- Compilation Flow - How data flows through the SDK
Further Reading
- Hexagonal Architecture - Original concept by Alistair Cockburn
- CONTRIBUTING.md - Contribution guidelines