7 documents covering stack, integrations, architecture, structure, conventions, testing, and concerns.
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
PlatformClientProtocol; the current implementation (sdk/mock.py) is swappable without touching core or adapters - All state is stored through a
StateStoreProtocol, withInMemoryStorefor tests andSQLiteStorefor 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 aliasesIncomingEventandOutgoingEvent - 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.py—EventDispatcher: routesIncomingEventto registered handler functions; returnslist[OutgoingEvent]core/handlers/— one module per event category (start,message,chat,settings,callback)core/store.py—StateStoreProtocol +InMemoryStore+SQLiteStorecore/chat.py—ChatManager: creates/renames/archives chat workspaces (C1/C2/C3); persists viaStateStorecore/auth.py—AuthManager: tracks auth flow state (pending→confirmed); persists viaStateStorecore/settings.py—SettingsManager: 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.py—PlatformClientProtocol:get_or_create_user,send_message,stream_message,get_settings,update_settings; alsoWebhookReceiverProtocol, Pydantic models (User,MessageResponse,MessageChunk,UserSettings,AgentEvent)sdk/mock.py—MockPlatformClient: full in-memory implementation with simulated latency; supports both sync (send_message) and streaming (stream_message, currently returns single chunk); includes webhook simulation viasimulate_agent_event()
- Depends on:
sdk/interface.py - Used by:
core/managers, adapters during bot startup
Adapter Layer:
- Purpose: Translates platform-native events into
IncomingEventandOutgoingEventback 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), optionalstore.py/room_router.py/reactions.pyfor adapter state - Depends on:
core/,sdk/, platform SDK (aiogram or matrix-nio) - Used by:
__main__/asyncio.run(main())
Data Flow
Incoming Message (Matrix example):
matrix-niofiresRoomMessageTextcallback →MatrixBot.on_room_message()inadapter/matrix/bot.pyresolve_chat_id()inadapter/matrix/room_router.pymapsroom_id→ logicalchat_id(e.g.C1), persisted inStateStorefrom_room_event()inadapter/matrix/converter.pyconverts the nio event toIncomingMessageorIncomingCommandEventDispatcher.dispatch(incoming)incore/handler.pyselects the handler by routing key (command name, callback action, or"*"for messages)- Handler (e.g.
core/handlers/message.py:handle_message) callsplatform.send_message()onMockPlatformClient, receivesMessageResponse - Handler returns
list[OutgoingEvent](e.g.[OutgoingTyping(..., False), OutgoingMessage(...)]) MatrixBot._send_all()iterates the list;send_outgoing()converts each to aclient.room_send()/client.room_typing()call
Incoming Reaction (Matrix):
ReactionEventcallback →MatrixBot.on_reaction()from_reaction()maps emoji key toIncomingCallbackwithaction="confirm","cancel", or"toggle_skill"- Dispatch →
core/handlers/callback.py
Command Routing:
The EventDispatcher uses a routing key per event type:
IncomingCommand→event.command(e.g."start","new","settings")IncomingCallback→event.action(e.g."confirm","toggle_skill")IncomingMessage→"*"(catch-all), orevent.attachments[0].typeif 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 useInMemoryStore
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/keysinterface - 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:
MockPlatformClientinsdk/mock.py - Swap path: Replace
sdk/mock.pywith 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→ buildAsyncClient→build_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 warningAuthManagerandChatManagerraiseValueErrorfor not-found entities; callers are responsible for catchingMockPlatformClientraisesPlatformError(defined insdk/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