surfaces/.planning/codebase/ARCHITECTURE.md
Mikhail Putilovskij c9072d51ea docs: add codebase map to .planning/codebase/
7 documents covering stack, integrations, architecture, structure,
conventions, testing, and concerns.
2026-04-02 00:00:51 +03:00

7.7 KiB

Architecture

Analysis Date: 2026-04-01

Pattern Overview

Overall: Hexagonal / Ports-and-Adapters

Key Characteristics:

  • A platform-neutral core/ defines all business logic and unified event types
  • Adapters (adapter/telegram/, adapter/matrix/) translate platform-specific events into core types and back
  • The AI platform SDK is hidden behind a PlatformClient Protocol; the current implementation (sdk/mock.py) is swappable without touching core or adapters
  • All state is stored through a StateStore Protocol, with InMemoryStore for tests and SQLiteStore for production

Layers

Protocol Layer:

  • Purpose: Defines every data structure crossing layer boundaries
  • Location: core/protocol.py
  • Contains: IncomingMessage, IncomingCommand, IncomingCallback, OutgoingMessage, OutgoingUI, OutgoingNotification, OutgoingTyping, ChatContext, AuthFlow, SettingsAction, type aliases IncomingEvent and OutgoingEvent
  • Depends on: Python stdlib only
  • Used by: All other layers

Core / Business Logic Layer:

  • Purpose: Handles all domain logic independent of any platform
  • Location: core/
  • Contains:
    • core/handler.pyEventDispatcher: routes IncomingEvent to registered handler functions; returns list[OutgoingEvent]
    • core/handlers/ — one module per event category (start, message, chat, settings, callback)
    • core/store.pyStateStore Protocol + InMemoryStore + SQLiteStore
    • core/chat.pyChatManager: creates/renames/archives chat workspaces (C1/C2/C3); persists via StateStore
    • core/auth.pyAuthManager: tracks auth flow state (pendingconfirmed); persists via StateStore
    • core/settings.pySettingsManager: fetches/caches user settings from SDK; invalidates on write
  • Depends on: core/protocol.py, sdk/interface.py (Protocol only), core/store.py
  • Used by: Adapters

SDK / Platform Layer:

  • Purpose: Wraps the external Lambda AI platform; isolated behind a Protocol
  • Location: sdk/
  • Contains:
    • sdk/interface.pyPlatformClient Protocol: get_or_create_user, send_message, stream_message, get_settings, update_settings; also WebhookReceiver Protocol, Pydantic models (User, MessageResponse, MessageChunk, UserSettings, AgentEvent)
    • sdk/mock.pyMockPlatformClient: full in-memory implementation with simulated latency; supports both sync (send_message) and streaming (stream_message, currently returns single chunk); includes webhook simulation via simulate_agent_event()
  • Depends on: sdk/interface.py
  • Used by: core/ managers, adapters during bot startup

Adapter Layer:

  • Purpose: Translates platform-native events into IncomingEvent and OutgoingEvent back to platform-native calls
  • Location: adapter/matrix/, adapter/telegram/ (in .worktrees/telegram/)
  • Contains per adapter: bot.py (entry point + send logic), converter.py (native event → protocol), handlers/ (adapter-specific handler overrides registered on top of core handlers), optional store.py / room_router.py / reactions.py for adapter state
  • Depends on: core/, sdk/, platform SDK (aiogram or matrix-nio)
  • Used by: __main__ / asyncio.run(main())

Data Flow

Incoming Message (Matrix example):

  1. matrix-nio fires RoomMessageText callback → MatrixBot.on_room_message() in adapter/matrix/bot.py
  2. resolve_chat_id() in adapter/matrix/room_router.py maps room_id → logical chat_id (e.g. C1), persisted in StateStore
  3. from_room_event() in adapter/matrix/converter.py converts the nio event to IncomingMessage or IncomingCommand
  4. EventDispatcher.dispatch(incoming) in core/handler.py selects the handler by routing key (command name, callback action, or "*" for messages)
  5. Handler (e.g. core/handlers/message.py:handle_message) calls platform.send_message() on MockPlatformClient, receives MessageResponse
  6. Handler returns list[OutgoingEvent] (e.g. [OutgoingTyping(..., False), OutgoingMessage(...)])
  7. MatrixBot._send_all() iterates the list; send_outgoing() converts each to a client.room_send() / client.room_typing() call

Incoming Reaction (Matrix):

  1. ReactionEvent callback → MatrixBot.on_reaction()
  2. from_reaction() maps emoji key to IncomingCallback with action="confirm", "cancel", or "toggle_skill"
  3. Dispatch → core/handlers/callback.py

Command Routing:

The EventDispatcher uses a routing key per event type:

  • IncomingCommandevent.command (e.g. "start", "new", "settings")
  • IncomingCallbackevent.action (e.g. "confirm", "toggle_skill")
  • IncomingMessage"*" (catch-all), or event.attachments[0].type if attachments present

Adapters call register_all(dispatcher) first (core handlers), then register_matrix_handlers(dispatcher, ...) to override or add platform-specific variants (e.g. !new creates a real Matrix room via the nio client).

State Management:

  • All persistent state goes through StateStore (key-value, async interface)
  • Key namespaces: chat:{user_id}:{chat_id}, auth:{user_id}, settings:{user_id}, matrix_room:{room_id}, matrix_user:{matrix_user_id}, matrix_state:{room_id}, matrix_skills_msg:{room_id}
  • Production uses SQLiteStore (row-per-key, JSON-serialised values); tests use InMemoryStore

Key Abstractions

EventDispatcher (core/handler.py):

  • Purpose: Single dispatch table for all event types; decouples handler logic from transport
  • Pattern: Registry (map of event_type → {key → HandlerFn}); wildcard "*" as fallback
  • Handler signature: async def handler(event, chat_mgr, auth_mgr, settings_mgr, platform) → list[OutgoingEvent]

StateStore Protocol (core/store.py):

  • Purpose: Pluggable persistence behind a minimal get/set/delete/keys interface
  • Implementations: InMemoryStore (tests/dev), SQLiteStore (production)
  • Key pattern: "{namespace}:{discriminator}"

PlatformClient Protocol (sdk/interface.py):

  • Purpose: Contracts the entire surface of the Lambda AI SDK
  • Current implementation: MockPlatformClient in sdk/mock.py
  • Swap path: Replace sdk/mock.py with a real SDK client; no changes needed elsewhere

Converter functions (adapter/matrix/converter.py):

  • Purpose: One-way transformation from platform-native event to IncomingEvent
  • Always produce canonical protocol types; adapters never pass raw library objects to core

Entry Points

Matrix Bot:

  • Location: adapter/matrix/bot.py:main()
  • Run: python -m adapter.matrix.bot
  • Startup sequence: load .env → build AsyncClientbuild_runtime() → register callbacks → client.sync_forever()

Telegram Bot:

  • Location: .worktrees/telegram/adapter/telegram/bot.py (feature branch, not merged to main yet)
  • Run: python -m adapter.telegram.bot

Error Handling

Strategy: Errors propagate up to the adapter's event callback. The adapter logs and drops the event; the bot keeps running.

Patterns:

  • EventDispatcher.dispatch() returns [] (empty list) when no handler is found and logs a warning
  • AuthManager and ChatManager raise ValueError for not-found entities; callers are responsible for catching
  • MockPlatformClient raises PlatformError (defined in sdk/interface.py) on unexpected states

Cross-Cutting Concerns

Logging: structlog throughout; all managers and the dispatcher use structlog.get_logger(__name__) Validation: Pydantic models in sdk/interface.py for SDK responses; plain dataclasses in core/protocol.py for internal events Authentication: AuthManager.is_authenticated() is checked in handle_message before forwarding to platform; unauthenticated users receive a prompt to run !start / /start


Architecture analysis: 2026-04-01