Skip to content

feat(slack): native streaming via chat.startStream/appendStream/stopStream - #66542

Closed
ashah360 wants to merge 1 commit into
NousResearch:mainfrom
ashah360:feat/slack-native-streaming
Closed

feat(slack): native streaming via chat.startStream/appendStream/stopStream#66542
ashah360 wants to merge 1 commit into
NousResearch:mainfrom
ashah360:feat/slack-native-streaming

Conversation

@ashah360

Copy link
Copy Markdown
Contributor

What

Slack's Agents & AI Apps feature ships a native streaming surface (chat.startStream / chat.appendStream / chat.stopStream) that renders a live-typing message — much smoother than the edit-based progressive updates the Slack adapter uses today. The bundled slack_sdk already supports these methods; this PR wires them into the adapter through the existing draft-streaming interface (supports_draft_streaming / send_draft), the same one Telegram drafts use. No consumer or config changes.

How

  • supports_draft_streaming() opts in whenever the app is connected and native streaming hasn't been detected as unavailable.
  • send_draft() starts a stream on the first frame (anchored to the resolved thread_ts, with recipient_team_id/recipient_user_id for channel streams) and appends only the delta on subsequent frames (the streaming API is append-only). The consumer's trailing cursor glyph is stripped before delta computation.
  • Finalization differs from Telegram by design: Telegram drafts are ephemeral and the consumer delivers the final answer as a fresh sendMessage. A Slack stream IS the final message, so send() intercepts the turn-final delivery for a chat with an active stream whose streamed text is a prefix of the final content, and seals it via chat.stopStream with the remaining delta instead of posting a duplicate. Opt-in Block Kit is applied to the sealed message via chat_update, mirroring edit_message's finalize path. Unrelated sends (interim commentary) pass through untouched.
  • Feature-gate errors from chat.startStream (not_allowed, missing_scope, unknown_method, …) are cached on the adapter so subsequent runs skip straight to edit-based streaming, with a single warning naming the fix (enable Agents & AI Apps for the app). Transient errors only disable drafts for the current run via the consumer's existing send_draft failure handling.
  • Segment breaks (new draft_id) and disconnect() seal any open stream so chats are never left with a dangling live-typing indicator.

Fallback behavior

Apps without the AI feature enabled: first streaming attempt fails, adapter caches unsupported, run falls back to edit-based streaming mid-flight (consumer's existing path), all future runs go straight to edits. One warning log total.

Testing

  • 18 new tests in tests/gateway/test_slack_native_streaming.py covering: stream start/append delta computation, cursor stripping, no-op frames, prefix-mismatch fallback, feature-gate error caching vs transient errors, turn-final sealing (no duplicate post), unrelated-send passthrough, stopStream-failure fallback to a normal post, Block Kit pass after seal, and disconnect cleanup.
  • Existing Slack suites pass: test_slack.py, test_slack_block_kit_adapter.py, plus all -k stream gateway tests (402 passed).
  • Validated end-to-end against a live Slack workspace with an AI-enabled app: responses stream natively and finalize as a single message; verified the streamed message seals with correct final content.

…tream

Slack's Agents & AI Apps feature ships a native streaming surface that
renders a live-typing message instead of the edit-based progressive
updates the adapter used until now.

The adapter now implements the existing draft-streaming interface:

- supports_draft_streaming() opts in whenever the app is connected and
  native streaming hasn't been detected as unavailable.
- send_draft() starts a stream on the first frame (chat.startStream,
  anchored to the resolved thread_ts, with recipient_team_id/user_id
  for channel streams) and appends only the delta on subsequent frames
  (chat.appendStream is append-only). The consumer's trailing cursor
  glyph is stripped before delta computation.
- Unlike Telegram drafts (ephemeral, replaced by a real sendMessage),
  a Slack stream IS the final message. send() therefore intercepts the
  turn-final delivery for a chat with an active stream whose streamed
  text is a prefix of the final content, and seals it via
  chat.stopStream with the remaining delta instead of posting a
  duplicate. Rich Block Kit (when enabled) is applied to the sealed
  message via chat_update, mirroring the finalize path in edit_message.
- Feature-gate errors from chat.startStream (not_allowed,
  missing_scope, unknown_method, ...) are cached on the adapter so
  subsequent runs skip straight to edit-based streaming with a single
  warning naming the fix (enable Agents & AI Apps for the app);
  transient errors only disable drafts for the current run via the
  consumer's existing send_draft failure handling.
- Segment breaks (new draft_id) and disconnect() seal any open stream
  so chats are never left with a dangling live-typing indicator.

No consumer or config changes: streaming.transport auto/draft now
lights up native streaming on Slack through the same interface
Telegram drafts use, and the edit-based path remains the fallback.
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins platform/slack Slack app adapter P3 Low — cosmetic, nice to have needs-decision Awaiting maintainer decision before any implementation sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 17, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #48066: both implement native Slack reply streaming, but this PR targets the current plugin adapter and includes distinct stream-lifecycle/fallback handling. Maintainers should choose or consolidate the active implementation.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for building this on the existing draft-stream interface. Native Slack streaming is still absent on current main: gateway/platforms/base.py:2537-2554 defaults the capability off, and gateway/config.py:625-631 identifies Slack as edit-only today.

Problems

  • Blocking: 10fc71dc7d6a keys _active_streams by chat_id and uses channel-only client lookup for stream lifecycle calls. Current Slack sends route by explicit team metadata at plugins/platforms/slack/adapter.py:1390-1397; tests/gateway/test_slack.py:2446-2486 covers the same D_SHARED channel across workspaces. Scope stream state and client selection by workspace so one tenant cannot append/seal another tenant's stream.
  • Blocking: the new send_draft() exception path drops an existing stream without stopStream. The consumer disables drafts after a failed frame (gateway/stream_consumer.py:1302-1339) and falls back to regular delivery (gateway/stream_consumer.py:1674-1701), so an append failure after a successful start can leave a dangling stream and then post a second final message.

Suggested changes

  • Preserve workspace routing metadata/client for every stream lifecycle operation and add a shared-channel multi-workspace test.
  • Best-effort seal an already-started stream on append failure before fallback, with a regression test.

This is an automated hermes-sweeper review.

# {"ts": str, "draft_id": int, "sent": str, "started": float}
# ``sent`` is the raw (pre-mrkdwn) text streamed so far — deltas are
# computed against it because the streaming API is append-only.
self._active_streams: Dict[str, Dict[str, Any]] = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chat_id is not a safe stream key for this adapter's multi-workspace mode. Existing Slack tests use the same D_SHARED channel in two workspaces, and _channel_team is only a mutable fallback; key this state by team plus channel (and preserve that routing identity for stop/update) to prevent one workspace from sealing or updating another workspace's stream.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/streaming Streaming responses: gateway delivery, provider wire labels Jul 18, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #85476 — your commit was cherry-picked onto current main with your authorship preserved in git log. Thanks for the clean draft-interface implementation; it shipped essentially as-is, with the task-card progress feature layered on top. Closing this PR as landed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/streaming Streaming responses: gateway delivery, provider wire comp/plugins Plugin system and bundled plugins needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have platform/slack Slack app adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants