Skip to content

Platform adapters

How Slack, Discord, Telegram, and GitHub adapters convert platform events into mikan's shared runtime format.

Their goal is to keep src/runtime/* and src/agent.ts from knowing each platform’s SDK, thread rules, message update API, or file download behavior.

Event normalization

Convert native platform messages, mentions, slash commands, or replies into a shared ConversationEvent.

Session routing

Compute sessionKey from channels, threads, and reply chains so conversations enter the right session.

Response capability

ConversationResponder wraps replies, updates, typing/working state, and file uploads.

  1. Receive platform events such as messages, mentions, slash commands, or replies.
  2. Decide whether the message should trigger mikan: DM, mention, thread reply, or auto-reply policy.
  3. Convert it to shared ConversationEvent and ConversationMessage values.
  4. Compute sessionKey so different channels, threads, and replies map to the right session.
  5. Download attachments into the conversation office’s attachments/ directory.
  6. Create a ConversationResponder that wraps replies, updates, typing/working state, and file uploads.
  7. Hand the event to MessagingEventHandler, which is ConversationRuntime.

Shared types are exported from src/adapter.ts; implementation types live in src/types.ts and src/adapters/types.ts.

Adapters keep their platform’s raw conversation id at the external I/O boundary — that is what they receive from the SDK and what they send back. Storage does not: platform plus the raw id form an OfficeAddress, which the src/office/ module hashes into the office key naming the conversation’s directory, its host-side state, and its vault. Adapters do not construct those paths themselves; they pass the address and receive an Office value. One consequence worth knowing when reading disk: a Slack channel C0123456789 lives at <workspace>/v1-slack-c0123456789-<digest>/, not at <workspace>/C0123456789/.

Slash-command registration and routing derive from src/commands/manifest.ts rather than per-adapter inventories, so a new command reaches every platform that opts in from one entry.

sequenceDiagram
participant P as Platform SDK
participant A as Platform adapter
participant I as processMessageIntake()
participant R as ConversationRuntime
participant C as ConversationResponder
P->>A: native message / slash command / reply
A->>A: parse conversation, user, thread, attachments
A->>I: ConversationEvent base + trigger/log/attachment hooks
I->>I: auto-reply / trigger decision
I->>A: download attachments when needed
I->>R: handler.handleEvent(event, bot, context)
R->>C: respond / replaceResponse / setWorking / uploadFile
C->>P: platform formatting and send
FilePurpose
src/adapter.tsExports shared interfaces such as MessagingBot, ConversationEvent, ConversationMessage, and ConversationResponder.
src/adapters/intake.tsShared message intake flow: magic word, trigger policy, attachments, log, busy policy, queue, then dispatch.
src/adapters/shared.tsShared retry, queue, long message splitting, stop target resolution, and the two office write paths (log.jsonl, attachments).
src/adapters/progressive-renderer.tsOwns progressive response buffering, tool progress, finalization, platform write serialization, and response-error reporting.

saveIncomingAttachments() owns the whole attachment convention — a sanitized <timestamp>_<name> under the office’s attachments/ directory and an office-relative path handed to the runtime — so no adapter composes conversation paths itself.

The core runtime depends only on these shared abstractions:

  • ConversationEvent: one user or platform event.
  • MessagingBot: platform messaging bot capabilities, such as post message and upload file.
  • ConversationContext: message, responder, and platform metadata attached to an event.
  • ConversationResponder: the channel used by the agent to reply.

When adding a platform, implement these abstractions first. Do not bring platform SDK details into src/runtime/* or src/agent.ts.