docs: ADR for Multi-Platform Adapter Architecture#94
Conversation
Defines ChatAdapter trait, AdapterRouter, and platform-specific implementations for Discord, Telegram, and Slack. Covers: trait design, config format, message size handling, reaction mapping, security, 5 implementation phases. Relates to openabdev#86 openabdev#93
dogzzdogzz
left a comment
There was a problem hiding this comment.
Review: RFC 002 — Multi-Platform Adapter Architecture
Overall direction is solid — ChatAdapter trait + AdapterRouter is the right abstraction. Phase 1 (pure refactor, no behavior change) is a good de-risking strategy. A few architectural gaps to address before this RFC is ready for implementation:
1. ChatAdapter trait missing message_limit()
Section 5 discusses per-platform message limits (2000/4096/4000), and format::split_message already accepts a limit: usize parameter. But the trait itself doesn't expose this — AdapterRouter would need to hardcode a match on the platform() string. Add:
fn message_limit(&self) -> usize;2. ChannelRef threading abstraction doesn't cover all models
Current design:
pub struct ChannelRef {
pub platform: String,
pub channel_id: String,
pub parent_id: Option<String>,
}This works for Discord (thread = separate channel with a parent), but not for platforms where threads are reply chains within the same channel, identified by a separate thread/topic ID rather than a child channel. Consider:
pub struct ChannelRef {
pub platform: String,
pub channel_id: String,
pub thread_id: Option<String>, // thread within channel (e.g. Slack thread_ts, Telegram topic_id)
pub parent_id: Option<String>, // parent channel if thread-as-channel (Discord)
}Alternatively, unify with a single thread_id: Option<String> and let each adapter interpret it. Either way, the current model needs to explicitly address the "thread as reply chain" vs "thread as child channel" distinction.
3. create_thread — who decides "already in a thread"?
Current discord.rs has logic in get_or_create_thread() that checks whether a message is already inside a thread (by comparing channel kind / parent ID). In the proposed architecture, where does this decision live?
- If in
AdapterRouter→ router needsChannelRef.is_thread()(e.g.thread_id.is_some() || parent_id.is_some()) - If in each adapter → duplicated logic
Recommend making this a router responsibility, with ChannelRef providing enough information to decide.
4. StatusReactionController is coupled to Serenity
The current StatusReactionController::new() takes Arc<serenity::http::Http>, ChannelId, and MessageId — all Serenity types. Phase 1 refactor must decouple this. Two options:
- (a) Controller calls
adapter.add_reaction()/adapter.remove_reaction()viaArc<dyn ChatAdapter> - (b) Introduce a small
ReactionSendertrait
Option (a) is simpler and aligns with the existing trait design.
5. AdapterRouter.handle_message() ownership model
The proposed signature:
pub async fn handle_message(
&self,
adapter: &dyn ChatAdapter,
...
) -> Result<()>;The current streaming loop in discord.rs uses tokio::spawn for the edit-streaming background task. A &dyn ChatAdapter reference cannot be moved into a spawned task. This needs to be Arc<dyn ChatAdapter>, or the streaming loop needs to be restructured to avoid spawning. This is a core design decision that affects all adapters.
6. Emoji format is not universal
ReactionsConfig defaults use Unicode emoji (👀, 🤔, 🆗). Some platforms require platform-specific names for the same emoji (e.g. eyes, thinking_face, ok). The RFC should clarify whether:
- (a) Config stores Unicode and each adapter converts internally
- (b) Config stores per-platform emoji mappings
- (c)
ChatAdapterexposes anfn emoji_name(&self, unicode: &str) -> Stringmethod
Recommend (a) for simplicity — adapters handle conversion as an internal concern.
7. Open Questions need recommended answers
The 5 open questions need recommendations, not just options:
| # | Question | Recommendation | Rationale |
|---|---|---|---|
| 1 | Shared vs separate SessionPool | Shared | Already namespaced by platform prefix; separate pools add complexity with no isolation benefit |
| 2 | Platform prefix in session keys | Yes, always prefix | IDs from different platforms can theoretically collide; prefix cost is negligible |
| 3 | Global vs per-adapter max_sessions | Global hard cap + per-adapter soft limit | Prevents one noisy platform from starving others |
| 4 | Rich messages in v1 | Text-only | Ship the abstraction first; rich messages are additive and platform-divergent |
| 5 | Platform-specific features | Defer to Phase 5 | Keep ChatAdapter minimal for v1; extend via PlatformExt traits or feature flags later |
8. Testing strategy missing
No mention of how to validate the architecture. Recommend:
- Define a
MockAdapter(in-memory, no network) that implementsChatAdapter - Use it to unit test
AdapterRouterlogic (session routing, streaming, reactions) in isolation - Phase 1 acceptance criteria: existing Discord behavior passes integration tests through the new trait boundary
Summary: The RFC is directionally correct. The main gaps are in trait completeness (message_limit, ChannelRef threading model), ownership model (Arc<dyn> vs &dyn), Serenity decoupling in reactions.rs, and missing test strategy. Recommend addressing these before Phase 1 implementation begins.
Question: Primary use case — pluggable single-platform or simultaneous multi-platform?Before Phase 1 implementation begins, I want to raise a definition question that significantly impacts the architecture scope. The RFC design — session key namespacing ( There are two meaningfully different use cases: A) Pluggable single-platform per deployment
B) Simultaneous multi-platform in one binary
These lead to meaningfully different implementations. If A is the primary case, the RFC is paying upfront complexity cost for an edge case — session namespacing and shared pool coordination are only needed when two adapters actually run concurrently. The simultaneous case (B) does have legitimate scenarios — e.g., an internal Slack team + an external Discord developer community both accessing the same agent backend with different Why this matters before Phase 1If the answer is B, there are two architectural pre-requisites not currently in the RFC that become hard blockers under concurrent load:
Neither of these is a Phase 1 concern if the answer is A. Proposed resolutionCould the RFC explicitly state which is the primary contract? If B, what is the concrete production scenario that justifies the added complexity? If A, the Phase 1 scope simplifies significantly and the namespacing / pool-sharing questions become post-MVP. Happy to elaborate on either the lock issue or the timeout issue if helpful. |
Introduces a platform-agnostic adapter layer (ChatAdapter trait + AdapterRouter) that decouples session management, streaming, and reactions from any specific chat platform. Refactors Discord support to implement this trait and adds a new Slack adapter using Socket Mode (WebSocket) with auto-reconnect. - Extract ChatAdapter trait with send/edit/thread/react methods + message_limit() - Extract AdapterRouter with shared session routing, edit-streaming, and reactions - Decouple StatusReactionController from serenity types (uses Arc<dyn ChatAdapter>) - Implement DiscordAdapter (ChatAdapter for Discord via serenity) - Implement SlackAdapter (ChatAdapter for Slack via reqwest + tokio-tungstenite) - Add [slack] config section (bot_token, app_token, allowed_channels, allowed_users) - Support running Discord + Slack simultaneously in one process - Session keys namespaced by platform (discord:xxx, slack:xxx) - Unicode emoji → Slack short name mapping for reactions Relates to openabdev#93, openabdev#94, openabdev#86 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a platform-agnostic adapter layer (ChatAdapter trait + AdapterRouter) that decouples session management, streaming, and reactions from any specific chat platform. Refactors Discord support to implement this trait and adds a new Slack adapter using Socket Mode (WebSocket) with auto-reconnect. - Extract ChatAdapter trait with send/edit/thread/react methods + message_limit() - Extract AdapterRouter with shared session routing, edit-streaming, and reactions - Decouple StatusReactionController from serenity types (uses Arc<dyn ChatAdapter>) - Implement DiscordAdapter (ChatAdapter for Discord via serenity) - Implement SlackAdapter (ChatAdapter for Slack via reqwest + tokio-tungstenite) - Add [slack] config section (bot_token, app_token, allowed_channels, allowed_users) - Support running Discord + Slack simultaneously in one process - Session keys namespaced by platform (discord:xxx, slack:xxx) - Unicode emoji → Slack short name mapping for reactions Relates to openabdev#93, openabdev#94, openabdev#86 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a platform-agnostic adapter layer (ChatAdapter trait + AdapterRouter) that decouples session management, streaming, and reactions from any specific chat platform. Refactors Discord support to implement this trait and adds a new Slack adapter using Socket Mode (WebSocket) with auto-reconnect. - Extract ChatAdapter trait with send/edit/thread/react methods + message_limit() - Extract AdapterRouter with shared session routing, edit-streaming, and reactions - Decouple StatusReactionController from serenity types (uses Arc<dyn ChatAdapter>) - Implement DiscordAdapter (ChatAdapter for Discord via serenity) - Implement SlackAdapter (ChatAdapter for Slack via reqwest + tokio-tungstenite) - Add [slack] config section (bot_token, app_token, allowed_channels, allowed_users) - Support running Discord + Slack simultaneously in one process - Session keys namespaced by platform (discord:xxx, slack:xxx) - Unicode emoji → Slack short name mapping for reactions Relates to openabdev#93, openabdev#94, openabdev#86 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a platform-agnostic adapter layer (ChatAdapter trait + AdapterRouter) that decouples session management, streaming, and reactions from any specific chat platform. Refactors Discord support to implement this trait and adds a new Slack adapter using Socket Mode (WebSocket) with auto-reconnect. - Extract ChatAdapter trait with send/edit/thread/react methods + message_limit() - Extract AdapterRouter with shared session routing, edit-streaming, and reactions - Decouple StatusReactionController from serenity types (uses Arc<dyn ChatAdapter>) - Implement DiscordAdapter (ChatAdapter for Discord via serenity) - Implement SlackAdapter (ChatAdapter for Slack via reqwest + tokio-tungstenite) - Add [slack] config section (bot_token, app_token, allowed_channels, allowed_users) - Support running Discord + Slack simultaneously in one process - Session keys namespaced by platform (discord:xxx, slack:xxx) - Unicode emoji → Slack short name mapping for reactions Relates to openabdev#93, openabdev#94, openabdev#86 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a platform-agnostic adapter layer (ChatAdapter trait + AdapterRouter) that decouples session management, streaming, and reactions from any specific chat platform. Refactors Discord support to implement this trait and adds a new Slack adapter using Socket Mode (WebSocket) with auto-reconnect. - Extract ChatAdapter trait with send/edit/thread/react methods + message_limit() - Extract AdapterRouter with shared session routing, edit-streaming, and reactions - Decouple StatusReactionController from serenity types (uses Arc<dyn ChatAdapter>) - Implement DiscordAdapter (ChatAdapter for Discord via serenity) - Implement SlackAdapter (ChatAdapter for Slack via reqwest + tokio-tungstenite) - Add [slack] config section (bot_token, app_token, allowed_channels, allowed_users) - Support running Discord + Slack simultaneously in one process - Session keys namespaced by platform (discord:xxx, slack:xxx) - Unicode emoji → Slack short name mapping for reactions Relates to openabdev#93, openabdev#94, openabdev#86 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduces a platform-agnostic adapter layer (ChatAdapter trait + AdapterRouter) that decouples session management, streaming, and reactions from any specific chat platform. Refactors Discord support to implement this trait and adds a new Slack adapter using Socket Mode (WebSocket) with auto-reconnect. - Extract ChatAdapter trait with send/edit/thread/react methods + message_limit() - Extract AdapterRouter with shared session routing, edit-streaming, and reactions - Decouple StatusReactionController from serenity types (uses Arc<dyn ChatAdapter>) - Implement DiscordAdapter (ChatAdapter for Discord via serenity) - Implement SlackAdapter (ChatAdapter for Slack via reqwest + tokio-tungstenite) - Add [slack] config section (bot_token, app_token, allowed_channels, allowed_users) - Support running Discord + Slack simultaneously in one process - Session keys namespaced by platform (discord:xxx, slack:xxx) - Unicode emoji → Slack short name mapping for reactions Relates to openabdev#93, openabdev#94, openabdev#86 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Declare Contract B (simultaneous multi-platform) as primary contract - Integrate dogzzdogzz 8 architectural review points: message_limit(), ChannelRef threading, Arc<dyn>, StatusReactionController decoupling, emoji handling, is_thread() helper - Answer all 5 open questions with recommendations (now section 11) - Add Testing Strategy section with MockAdapter (section 10) - Add Known Limitations section (section 12): pool lock contention resolved, streaming timeout open - Update implementation phases to reflect openabdev#259 merge status - Address antigenius0910 Contract A vs B question
|
All PRs must reference a prior Discord discussion to ensure community alignment before implementation. Please edit the PR description to include a link like: This PR will be automatically closed in 3 days if the link is not added. |
RFC 002 Updated — Review Feedback IntegratedUpdated the RFC to address all outstanding feedback from @dogzzdogzz and @antigenius0910, and to reflect the merge of #259 (Slack adapter). What changedPrimary contract declared: Contract B (simultaneous multi-platform)@antigenius0910 asked whether the RFC targets single-platform-per-deployment (A) or simultaneous multi-platform (B). Since #259 already merged with Discord + Slack running in one process, the RFC now explicitly declares B as the primary contract. Contract A is a subset that works automatically. @dogzzdogzz's 8 architectural points integrated
New sections added
Implementation phases updatedPhases now reflect reality: Phase 1 + Phase 3 marked as ✅ Merged (#259). Phase 4 repurposed for per-adapter session limits + streaming timeout. Phase 5 unchanged (platform-specific rich features). Commit
|
|
This RFC is still valid. Feel free to comment your thoughts and we'll discuss and update accordingly. |
Move multi-platform adapter architecture from docs/rfcs/ to docs/adr/ format, matching the existing ADR structure.
|
Thanks @antigenius0910 for the thorough Contract A vs B analysis — it was the right question to raise before Phase 1. The ADR has been updated to explicitly declare Contract B (simultaneous multi-platform) as the primary contract, since #259 already validated Discord + Slack running in one process. Contract A works automatically as a subset. Both architectural pre-requisites you flagged are addressed in the ADR:
The ADR has also been reformatted from RFC to ADR format under |
Summary
Architecture Decision Record for the multi-platform adapter architecture, reformatted from RFC 002 into the standard ADR format under
docs/adr/.What this covers
ChannelRef,MessageRef,SenderContextsupporting two threading modelsMockAdapterfor unit/integration testsFiles
docs/adr/multi-platform-adapters.mdRelated