# @mono-agent/slack-adapter

Connect an agent host to Slack Socket Mode with explicit channel authorization,
native interaction controls, and Slack-safe streamed delivery.

## Category

<!-- package-metadata:start -->
<!-- Generated by scripts/generate-package-docs.mjs. Do not edit by hand. -->

Category: `communication`
Tier: `core`
Catalog responsibility: Adapts Slack Socket Mode events to structural agent requests and streamed replies.

<!-- package-metadata:end -->

## Responsibility

Adapt Slack Socket Mode events into structural agent requests and streamed Slack replies. The package owns Slack-specific credentials, channel allowlists, mention cleanup, native model/effort controls, config-driven shortcuts and App Home actions, Web API calls, and Socket Mode message handling.

## Install / Usage

### Config-first host

`@mono-agent/agent-app` already includes this adapter. Keep Slack tokens in the
agent folder's `.env`, then enable the channel in `mono-agent.config.json`:

```dotenv
MONO_AGENT_SLACK_BOT_TOKEN=xoxb-replace-me
MONO_AGENT_SLACK_APP_TOKEN=xapp-replace-me
```

```json
{
  "slack": {
    "enabled": true,
    "allowedChannelIds": ["C0123"],
    "allowAllChannels": false
  }
}
```

```bash
mono-agent validate
mono-agent start --foreground
```

Adapter settings can be loaded from nested JSON under `slack` or explicit environment variables such as `MONO_AGENT_SLACK_BOT_TOKEN`, `MONO_AGENT_SLACK_APP_TOKEN`, and `MONO_AGENT_SLACK_ALLOWED_CHANNEL_IDS`.

The adapter is opt-in: `slack.enabled` / `MONO_AGENT_SLACK_ENABLED` defaults to `false`. While disabled the loader skips token validation and the channel reports `disabled` rather than `waiting_for_config`. Set `enabled: true` to turn it on; missing tokens or allowlist then surface as a real `waiting_for_config` reason.

### Programmatic use

Install the package directly only when composing a custom host:

```bash
pnpm add @mono-agent/slack-adapter
```

<!-- doc-test:typescript -->
```ts
import {
  startSlackAdapter,
  type AgentResponder,
} from "@mono-agent/slack-adapter";

const botToken = process.env.MONO_AGENT_SLACK_BOT_TOKEN;
const appToken = process.env.MONO_AGENT_SLACK_APP_TOKEN;
if (botToken === undefined || appToken === undefined) {
  throw new Error("Slack bot and app tokens are required.");
}

const responder: AgentResponder = {
  async respond(request) {
    return { text: `Slack request received: ${request.text}` };
  },
};

const slack = await startSlackAdapter({
  botToken,
  appToken,
  allowedChannelIds: ["C0123"],
  responder,
});

process.once("SIGINT", () => void slack.stop());
```

### Composition levels

Choose the highest useful abstraction:

1. The config-first `@mono-agent/agent-app` channel driver loads config, supplies
   runtime controls and history hooks, and owns channel status.
2. `startSlackAdapter` is the normal programmatic entrypoint. It builds the Web
   API client, discovers the authenticated bot identity, constructs the event
   adapter and Socket Mode runner, starts reconnection, and returns one `stop()`.
3. Eligible text sent during a same-conversation run enters the responder's
   acknowledged live-input path while retaining a normal-turn fallback slot.
4. Advanced hosts can compose `SlackWebApiClient`, `SlackAdapter`, and
   `SlackSocketModeRunner` separately when they need custom transport lifecycle.
5. `SlackMessageStream` and the Markdown functions are the lowest delivery and
   formatting layer; they do not receive or admit Slack events themselves.

### Activity indicator and transient tool ledger

While the agent works, the message stream surfaces progress in the thread. It
prefers Slack's official assistant-thread status — `assistant.threads.setStatus`
("App is _thinking…_"), which Slack auto-clears when the next message posts — and
falls back to a 👀 "seen" reaction on the triggering message. The status path only
applies inside a Slack **AI-assistant thread** and requires the app to have the
**Agents & AI Apps** feature enabled plus the **`assistant:write`** scope; in
regular channels/DMs (or without the scope) the call errors and the adapter uses
the reaction instead — no configuration needed for the fallback.

With final-only delivery, the first tool start posts one cumulative, secret-safe
activity message. Later starts edit it in place, adjacent duplicates collapse as
`(×N)`. A subagent stays expanded while it runs; at its first terminal event,
Slack removes all of that subagent's child tool lines while retaining its total
call count and duration. The compact row may include one secret-redacted
`Result` or `Reason` line capped at 120 Unicode code points. Concurrent subagents
collapse independently, and late events cannot re-expand a completed group. On
completion the adapter posts the final answer as a new message, then
best-effort deletes the activity message. A cleanup failure cannot duplicate or
lose the final answer, though it can leave the stale activity message behind.
Answer deltas and reasoning never enter that ledger. `ReadSkill` renders the
selected skill as `📚 Reading "<skill>"`
without exposing its path, while memory recall is preview-free as
`🧠 Recalling memory`. Memory writes remain `🧠 Updating memory`, and ordinary
file reads remain `📖 Reading`. Proactive
deliveries suppress it. An acknowledged `/cancel`
best-effort deletes the still-transient ledger and keeps the command's one
`Cancelled.` acknowledgement.

Applied live guidance adds a completed `↪️ Steered: “<safe preview>”` entry. If
a confirmed ledger already exists, Slack best-effort deletes and reposts the
same cumulative ledger so it becomes the newest bot message after the human
follow-up. A failed delete edits the existing ledger in place; neither path can
block or replace the final answer.

### Host-owned process-job lifecycle

The config-first host uses `SlackAdapter.updateProcessJob` for a background
Exec/Bash job's lifecycle card. This path never invokes the responder. It binds
the card to the exact channel/thread origin, serializes updates per job, edits
the same message when possible, and ignores any nonterminal update that arrives
after a terminal state. The adapter retains up to the shared 10,096 outstanding
lifecycle identities, refuses overflow rather than evicting live state, and
reclaims only terminal identities whose wake has already settled. When a
terminal update has no editable reference, the adapter makes at most one
self-contained terminal fallback post in that same thread. This identity state
is instance-local, not durable across an adapter/process restart; the existing
post-restart fallback contract is unchanged. Ordinary proactive `notify`
behavior is unchanged.

The app-owned completion wake uses `notify` with a stable delivery key and
`steerActive: true`. It reserves the thread's normal queue position first,
targets only the exact active run, and reports `steered` after provider
acknowledgement. Every explicit non-applied settlement runs the reserved normal
turn with visible thinking and tool activity; an unknown outcome is not
silently reported as success.

### Live follow-up steering

When the responder exposes live input, another plain-text message in the same
Slack **thread** while a turn is running is offered to that active provider run
and acknowledged with 👀. Commands, pending `AskUser` replies, and messages with
files retain their existing paths. The adapter reserves the follow-up's ordinary
queue position before offering it: if the provider is unsupported, the run ends
first, or delivery fails, the exact message runs next as a normal turn. Applied
guidance does not create a second assistant response. Provider acknowledgement
adds the completed `↪️ Steered` activity described above.

An offer is made only from the thread that owns the active run. Two physical
threads can still resolve to one conversation — a threaded proactive post, or a
recorded producing-conversation alias — and serializing them there is correct
because they share a session, but steering across them is not: it would fold a
message typed in one thread into a run streaming into another, leaving its
sender with a reaction instead of an answer. A run from a different thread, and
any cron or proactive run, is therefore never offered an inbound message; it
runs as its own queued turn and answers in its own thread.

### Model and effort controls

The built-in agent app supplies the Slack adapter with the configured primary
model and fallbacks, so no Slack-specific model list is required. Runtime
controls have two native entry points:

- Send `@agent /model` or `@agent /effort` as an ordinary mention message. In a
  shared channel this keeps the selection local to that Slack thread.
- Register `/<bot-username>-model` and `/<bot-username>-effort` as Slack Slash
  Commands. `startSlackAdapter` derives these exact names from `auth.test.user`,
  so a bot named `foo` handles `/foo-model` and `/foo-effort` without a
  mono-agent config field. Slack slash commands do not carry thread context, so
  shared-channel selections made this way apply across the channel. A thread's
  mention-command selection can still override the inherited channel choice.

`startSlackAdapter` discovers the authenticated bot user ID with `auth.test` and
merges it with any configured `botUserIds`. It also validates the authenticated
username before using it as readable model text. By default, all recognized self
forms are removed from the current turn except one marker at the first source
position: the first configured alias is kept verbatim, while a native mention is
rendered as `@<authenticated-username>` or falls back to its matched user ID.
Mentions inside inline or fenced code are untouched, and earlier thread context
keeps its existing rendering. A leading self identity is removed on a command-
recognition copy, so `@agent /model` still works without changing model-visible
text.

`stripMentionText` is deliberately tri-state. Omit it for the readable-marker
default, set `true` for legacy full stripping, or set `false` to keep raw Slack
mention forms. Operators who previously supplied only `botUserIds` received
implicit stripping; omission now selects readable-marker preservation, so set
`true` when that historical output is required.

Hosts constructing `SlackAdapter` directly may supply the additive optional
`SlackAdapterOptions.botUserName`; it must be the trusted authenticated username.
The adapter trims it, accepts 1–80 characters with no whitespace, angle bracket,
or backtick, and otherwise falls back to the matched bot user ID. Inline Slack
mention labels are never used as self-identity authority.

The same controls also accept exact arguments:

- `@agent /model default` or `@agent /model <exact-configured-ref>`
- `@agent /effort default` or `@agent /effort <supported-value>`
- `/<bot-username>-model default` or `/<bot-username>-model <exact-configured-ref>`
- `/<bot-username>-effort default` or `/<bot-username>-effort <supported-value>`

In a direct-message channel, a selection applies to every subsequent DM turn,
including new Slack threads. In public and private shared channels, slash-command
choices are channel-wide while mention-command choices are thread-local and take
precedence. Everyone using the same scope shares its selection. State is
process-local and resets on restart. Changing models also clears a selected
effort when the new model does not support it.

Model options use a short model identifier as the title and the exact configured
reference as descriptive text. Runtime-control plain text explicitly disables
Slack emoji expansion so colon-delimited references remain literal.

Enable **Interactivity & Shortcuts** in the Slack app so selector actions arrive
over Socket Mode. To expose commands in Slack's `/` picker, create the two Slash
Commands in the app configuration, add the `commands` bot scope, and reinstall
the app if Slack requests authorization; Socket Mode carries their payloads, so
no Request URL is needed. A direct programmatic adapter consumer can override
the derived names with `runtimeSlashCommands`, omit `runtimeControls` to leave
all runtime commands unbound, or supply a validated catalog through that option.
Slack static-select menus support at most 100 options; a larger catalog remains
selectable with the exact-argument form.

### Structured AskUser prompts

When the host supplies structured `AskUser` state, the adapter posts optional
context followed by one Block Kit question at a time. Each question has two or
three option buttons plus **Other**; multi-select adds **Done**. A typed reply in
the same thread is consumed as the custom answer before normal turn admission,
so the blocked model run resumes without deadlocking. Stale actions expire and
the configured Slack channel allowlist remains authoritative.

After the answer is recorded, Slack removes the interactive blocks and replaces
the question with the original labels for the selected options. A single answer
is summarized inline; multiple answers are listed by question header in answer
order. Unknown question or option IDs are omitted. A custom-only answer is shown
as `custom answer` in a multi-answer summary, while its text is never echoed; a
single custom-only or otherwise unresolved answer keeps the generic
`Answer recorded.` confirmation.

### Thread and channel context

An in-thread trigger reads that thread (`conversations.replies`); a top-level
channel mention or a DM reads recent history (`conversations.history`). The
result becomes the shared contract's `precedingMessages`, which the harness
renders as a bounded, fenced, explicitly untrusted transcript and never persists.

Slack caps these methods at roughly one request per minute and 15 objects for
non-Marketplace apps, so the adapter issues **exactly one** request per admitted
turn with no retries and no pagination, and latches a per-channel cooldown from
`Retry-After`. Slack's docs disagree with themselves about which end `limit`
truncates, so the replies path requests a page anchored at the trigger and
verifies the trigger came back; an unanchored page produces no transcript rather
than a misleading one. The whole phase is raced against `timeoutMs`, so a custom
client that ignores `options.signal` still cannot delay a turn.

The app's own posts are excluded by both user ID and `bot_id`. Other apps'
messages are included and labelled `isBot`, since a CI bot's failure is often why
someone pulled the agent in.

### Speaker names

A Slack event identifies its sender only by user ID. That ID doubles as a DM
channel ID, so it is an actionable delivery target and stays host-only — it never
reaches a prompt. With `resolveUserNames` on (the default) the adapter resolves
the sender's display name and handle through `users.info` and passes those as the
model-visible `sender`, so a shared-channel turn reads as `Alice Chen (@alice)`.

Requires the `users:read` bot scope. `user-directory.ts` caches 500 entries for
30 minutes (failed lookups for 5), bounds concurrency at 3, and latches the
lookup off for the process after one `missing_scope` failure so a mis-scoped app
pays a single call rather than one per speaker per turn. Every failure path leaves
the turn unnamed instead of failing it, which is byte-identical to the behaviour
before names existed. A resolved name is user-controlled, so it is evidence of a
name and never proof of identity.

Note that the name is durable: it becomes the stored conversation turn's speaker
label and the memory-capture label, so enabling this changes what later recalls
surface, not only the current prompt.

### Channel names

Every turn also tells the agent WHICH surface it is on: the kind (`dm`,
`channel`, or `group`), the surface's id, and — with `resolveChannelNames` on
(the default) — its name, resolved through `conversations.info`. The Session
block additionally states the per-message character budget and that a longer
answer continues in the thread.

Kind resolution falls back in descending authority: `conversations.info`
(`is_im`/`is_mpim`), the event's `channel_type`, then the channel-id prefix. The
prefix fallback is what covers `app_mention`, which carries no `channel_type`.

Requires `channels:read` (public) / `groups:read` (private).
`channel-directory.ts` mirrors `user-directory.ts`: 200 entries for 30 minutes,
5-minute negative TTL, never rejects, and latches off permanently on
`missing_scope`. Every failure leaves the surface named by kind and id.

Unlike a speaker id, the surface id IS model-visible — it is what disambiguates
the surface when no name resolves. The thread ts, the `replyTo` conversation id,
and the platform user id all remain host-only. Note the interaction with
`SlackSendMessage`: that tool takes a raw channel id, so a deployment running it
with `allowAllChannels` can post to any channel id the model has seen. An
explicit channel allowlist still bounds delivery.

### Bare mentions

Mentioning the app with no other text starts an ordinary turn instead of being
refused. The adapter substitutes `messages.bareMentionPrompt`, which tells the
agent to work the request out from the conversation it was pulled into. A message
with usable files may carry empty text plus attachments; a message whose files
were all skipped still receives `messages.unsupportedText`.

### Silent-delivery limitation

Programmatic proactive delivery accepts `silent: true` in both
`SlackNotifyOptions` and `SlackMessageStreamOptions` so channel integrations can
use a common option shape. Slack's `chat.postMessage` API has no bot-controlled
equivalent to Telegram's `disable_notification`, however. The adapter therefore
posts with normal Slack notification behavior and, when a logger is configured,
emits one explicit warning; it never forwards an invented `silent` field or
claims suppression succeeded. Slack client/workspace notification settings
remain authoritative. A caller that requires guaranteed quiet hours must skip
or defer the Slack delivery.

### Shortcuts and App Home

`slack.shortcuts` binds global or message shortcut callback IDs to prompts.
`slack.homeTab` publishes an optional header and prompt-running buttons when the
Home tab opens. Both fields are structured JSON-only configuration; they have no
environment-variable form. App Home defaults to disabled when `enabled` is
omitted, and `buttons` defaults to an empty array; an enabled header-only tab is
valid.

```json
{
  "slack": {
    "shortcuts": [
      {
        "callbackId": "triage_request",
        "prompt": "Prepare the daily support triage checklist.",
        "channelId": "C0123"
      }
    ],
    "homeTab": {
      "enabled": true,
      "headerText": "*Quick actions*",
      "buttons": [
        {
          "actionId": "build_digest",
          "label": "Build digest",
          "prompt": "Build today's team digest.",
          "channelId": "C0123"
        }
      ]
    }
  }
}
```

Destinations still pass the Slack channel allowlist. See the canonical
[Slack channel guide](https://mono-agent-docs.vercel.app/channels/slack/#shortcuts) for all fields,
routing behavior, and Slack app setup.

### Generated reply files

When the host returns an authorized reply-file part, the adapter uses Slack's
modern external upload sequence: request an upload URL, send the exact
integrity-checked bytes, then complete the upload in the destination
channel/thread. The bot token needs Slack's `files:write` scope. A file is
removed from textual fallback only after completion succeeds; an unavailable
API method or failed upload leaves a concise warning and never exposes the
local artifact path or private URL. Confirmed uploads are deduplicated by file
integrity plus channel/thread, including proactive retries.

Custom `SlackWebApi` implementations may omit the three optional external-file
methods and retain fallback-only behavior. See
[Reply files and MCP Apps](https://mono-agent-docs.vercel.app/tools/rich-replies/).

## Architecture

### Data flow

The request lifecycle is:

1. `config.ts` layers JSON and environment values and validates credentials,
   channel authorization, interaction bindings, and resilience tuning.
2. `start.ts` creates the authenticated Web API client, discovers the bot user,
   wires the adapter to the Socket Mode runner, and starts the reconnect loop.
3. `socket-mode-runner.ts` acknowledges envelopes, admits each exact nonblank
   Events API `event_id` at most once within its bounded instance-local window,
   and routes events, interactivity, and slash commands; `adapter.ts` authorizes
   and normalizes them into structural agent requests with per-conversation
   admission and live-input fallback reservation. Just before a turn is
   submitted, `user-directory.ts` resolves the speaker's model-visible name and
   `thread-context.ts` selects the preceding messages from one bounded
   conversation read; the phase is best-effort, raced against its own deadline,
   and cannot fail or delay the turn.
4. The host responder emits standard stream events. `message-stream.ts` converts
   them into Slack posts/updates/deletes, while `slack-markdown.ts` translates
   standard Markdown at the transport boundary.
5. Host-owned process-job projections bypass the responder and use the
   adapter-local monotonic lifecycle-message path.
6. `stop()` aborts the runner and waits for the connection loop to settle.

### Event callback admission

`SlackSocketModeRunner` acknowledges a valid Events API envelope before checking
and synchronously recording its exact, nonblank `event_id`. The in-memory cache
uses a fixed 10-minute TTL, does not refresh a hit, and retains at most 10,000
entries in insertion-order FIFO. Its state belongs to the runner instance: it
survives Socket Mode reconnects and repeated `start()` calls on that runner, but
a fresh runner or process starts empty. There is no persistent or distributed
dedupe state.

The guarantee is intentionally bounded. A delivery outside the 10-minute window
or whose ID was evicted at the cap can be admitted again; the runner warns once
when the cap forces that degradation. A blank or whitespace-only string ID fails
open after acknowledgement and is dispatched with a safe debug record, while
the existing malformed-callback guard continues to acknowledge and ignore an
absent or non-string ID. A custom connection runner that calls
`SlackEventCallbackHandler.handleEventCallback` must perform equivalent
at-most-once admission first.

### Package structure

| Source module | Responsibility |
| --- | --- |
| [`config.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/config.ts) | Config/env loading and validation. |
| [`start.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/start.ts) | Recommended programmatic composition root. |
| [`adapter.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/adapter.ts) | Authorization, event normalization, structured-ask presentation and pre-admission replies, commands, shortcuts, App Home, and admission. |
| [`runtime-controls.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/runtime-controls.ts) | Validated model/effort catalogs, callback tokens, menu blocks, and slash-command routing helpers. |
| [`socket-mode-runner.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/socket-mode-runner.ts) | Socket Mode acknowledgements, bounded event callback admission, heartbeat, degradation, and reconnect lifecycle. |
| [`slack-client.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/slack-client.ts) | Typed Slack Web API boundary and private-file downloads. |
| [`message-stream.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/message-stream.ts) | Final-only delivery, transient status, retry classification, and message limits. |
| [`slack-markdown.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/slack-markdown.ts) | Standard Markdown to Slack `mrkdwn` conversion and normalization. |
| [`user-directory.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/user-directory.ts) | Bounded `users.info` cache turning user IDs into model-visible speaker names. |
| [`thread-context.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/thread-context.ts) | Pure selection, bounding, and deadline logic for the preceding-message transcript. |
| [`types.ts`](https://github.com/robertsreberski/mono-agent/blob/main/packages/slack-adapter/src/types.ts) | Minimal Slack wire and client contracts. |

## Public API

### Start here

| API | Use it for |
| --- | --- |
| `loadSlackAdapterConfig` | Load and validate the `slack` config/env surface. |
| `startSlackAdapter` | Start the complete Web API + event adapter + Socket Mode lifecycle. |
| `SlackAdapter` | Normalize and handle events with a custom connection runner. |
| `SlackAdapter.updateProcessJob` | Post or monotonically update one exact-origin host lifecycle card without a model turn. |
| `SlackSocketModeRunner` | Own Socket Mode transport and reconnect policy separately. |
| `SlackWebApiClient` | Call the Slack Web API through the adapter's typed boundary. |
| `SlackMessageStream` | Deliver a responder stream to one Slack destination. |
| `formatMarkdownForSlack` / `normalizeSlackMarkdownToMarkdown` | Translate only at the Slack boundary. |

<!-- public-api-inventory:start -->
<!-- Generated by scripts/generate-public-api-docs.mjs. Do not edit by hand. -->

Every symbol exported by each public code entrypoint is listed below.

**`@mono-agent/slack-adapter`**

```text
AgentMessageStream
AgentRequest
AgentResponder
AgentResponse
LoadSlackAdapterConfigInput
SLACK_CONFIG_FIELDS
SLACK_MAX_MESSAGE_CHARS
SLACK_THREAD_CONTEXT_DEFAULT_MAX_MESSAGES
SLACK_THREAD_CONTEXT_DEFAULT_REQUEST_LIMIT
SLACK_THREAD_CONTEXT_DEFAULT_TIMEOUT_MS
SLACK_THREAD_CONTEXT_MAX_MESSAGES_CEILING
SLACK_THREAD_CONTEXT_RATE_LIMIT_COOLDOWN_MS
SLACK_THREAD_CONTEXT_REQUEST_LIMIT_CEILING
SerialQueueFullError
SlackAdapter
SlackAdapterConfig
SlackAdapterConfigError
SlackAdapterConfigErrorCode
SlackAdapterConfigErrorDetails
SlackAdapterLogger
SlackAdapterMessages
SlackAdapterOptions
SlackAdapterStartLogger
SlackAdapterStartOptions
SlackAdapterStartResult
SlackAdapterStreamOptions
SlackApiError
SlackApiErrorDetails
SlackApiErrorKind
SlackApiFactoryInput
SlackAppsConnectionsOpenResult
SlackAttachmentOptions
SlackAuthTestResult
SlackBlockAction
SlackBlockActionsPayload
SlackChannelId
SlackChatDeleteParams
SlackChatDeleteResult
SlackChatPostMessageParams
SlackChatPostMessageResult
SlackChatUpdateParams
SlackChatUpdateResult
SlackContinuationSynthesisInput
SlackConversationMessage
SlackConversationMessagesResult
SlackConversationsHistoryParams
SlackConversationsRepliesParams
SlackDeliveryError
SlackDeliveryReceipt
SlackDeliveryReceiptListener
SlackDownloadFileParams
SlackEventBase
SlackEventCallback
SlackEventCallbackHandler
SlackEventHandlingResult
SlackEventIgnoredReason
SlackFile
SlackFilesCompleteUploadExternalParams
SlackFilesCompleteUploadExternalResult
SlackFilesGetUploadUrlExternalParams
SlackFilesGetUploadUrlExternalResult
SlackFilesUploadExternalParams
SlackHomeButton
SlackHomeButtonConfig
SlackHomeTabConfig
SlackHomeTabOptions
SlackInteractionHandler
SlackInteractionHandlingResult
SlackInteractivityPayload
SlackMessageStream
SlackMessageStreamLogger
SlackMessageStreamOptions
SlackMessageTs
SlackNotifyOptions
SlackNotifyResult
SlackPendingAsks
SlackRequestMetadata
SlackRequestOptions
SlackRuntimeControls
SlackRuntimeEffortOption
SlackRuntimeModelOption
SlackRuntimeSlashCommands
SlackSendOutcome
SlackShortcutBinding
SlackShortcutConfig
SlackShortcutPayload
SlackSlashCommandHandler
SlackSlashCommandHandlingResult
SlackSlashCommandPayload
SlackSocketModeEnvelope
SlackSocketModeRunner
SlackSocketModeRunnerBackoffOptions
SlackSocketModeRunnerHeartbeatOptions
SlackSocketModeRunnerLogger
SlackSocketModeRunnerOptions
SlackSocketModeRunnerStartOptions
SlackThreadContextConfig
SlackThreadContextOptions
SlackThreadContextSkipReason
SlackTriggerKind
SlackUserId
SlackUsersInfoParams
SlackUsersInfoResult
SlackViewsPublishParams
SlackWebApi
SlackWebApiClient
SlackWebApiClientOptions
SlackWebSocketFactory
SlackWebSocketLike
classifySlackError
formatMarkdownForSlack
loadSlackAdapterConfig
normalizeSlackMarkdownToMarkdown
startSlackAdapter
```

<!-- public-api-inventory:end -->

## Dependency Boundary

This package may depend on `@mono-agent/agent-contracts` plus Slack transport dependencies. It must not depend on the agent harness, runtime adapter, operator surfaces, other communication adapters, or host/demo code.

## What This Package Does Not Own

It does not own model execution, memory, prompt context, tool policy, browser/terminal operator surfaces, Slack app provisioning, or workspace-level authorization policy beyond explicit local adapter allowlists.

## Related Documentation

- [Slack channel guide](https://mono-agent-docs.vercel.app/channels/slack/)
- [Slack team-bot playbook](https://mono-agent-docs.vercel.app/playbooks/slack-team-bot-mcp-tools/)
- [Delivery and send tools](https://mono-agent-docs.vercel.app/channels/delivery-and-send-tools/)
- [Reply files and MCP Apps](https://mono-agent-docs.vercel.app/tools/rich-replies/)
- [Custom channel adapters](https://mono-agent-docs.vercel.app/programmatic/custom-channels/)

## Verification

Run the package-local build, typecheck, and behavior tests:

```bash
pnpm --filter @mono-agent/slack-adapter run build
pnpm --filter @mono-agent/slack-adapter run typecheck
pnpm --filter @mono-agent/slack-adapter run test
```
