Skip to content

feat(slack): render rich stream events in Slack threads#2230

Merged
anubra266 merged 11 commits intomainfrom
feat/slack-rich-streaming
Feb 23, 2026
Merged

feat(slack): render rich stream events in Slack threads#2230
anubra266 merged 11 commits intomainfrom
feat/slack-rich-streaming

Conversation

@anubra266
Copy link
Copy Markdown
Contributor

Summary

Extends the Slack streaming event handler to render rich agent output — data components, artifacts, citations, tool errors, and summary breadcrumbs — as structured Slack messages. Adds file-upload fallback for oversized payloads and cleans up tool approval state on denial.

Motivation

Following the tool approval Slack integration (#2149), agents can now produce structured stream events beyond text: data-component, data-artifact, data-summary, and tool lifecycle signals (tool-input-available, tool-output-denied, tool-output-error). Without handling these events, they were silently dropped — resulting in missing content (sources, structured data) and no feedback when tools fail. This PR surfaces all of them in thread context.

Approach

New block builder functions in blocks/index.ts handle each event type and return Slack Block Kit payloads. The streaming loop in streaming.ts dispatches to the appropriate builder and either posts a message or uploads a file when the payload exceeds Slack's block character limit (~2900 chars). State is accumulated per stream:

  • A toolCallIdToName map links tool-input-available call IDs to names so that tool-output-error events can be labeled correctly.
  • Citations are deduplicated by URL and flushed as a single message after the stream ends, rather than mid-stream, to avoid interleaving with text chunks.
  • A richMessageCount cap (5) prevents flooding a thread when agents emit many data events.
  • Tool errors and summary breadcrumb labels are collected and appended to the streamer.stop() footer blocks.

Architectural decisions

Citations batched at end vs. inline: Citations are collected across the stream and posted as a single message after streamer.stop(). This keeps the main text thread uninterrupted and mirrors how web UIs typically render sources in a footer.

File upload fallback for large payloads: JSON payloads >2900 chars and descriptions >2900 chars are uploaded as files (uploadV2) rather than truncated. This preserves data fidelity at the cost of a slightly different UX (file attachment vs. inline block). Requires the new files:write OAuth scope in the manifest.

MAX_RICH_MESSAGES = 5: A hard cap prevents runaway threads when agents emit many data events. Citations are exempt since they're deduplicated and flushed once. This limit is a conservative starting point and can be tuned.

toolCallId on pendingApprovalMessages: The existing approval tracking array was extended with toolCallId so denial events can find and clean up the correct pending entry without linear name matching (names are not guaranteed unique per turn).

Changes

packages/agents-work-apps/src/slack/services/blocks/index.ts

  • Add buildToolOutputErrorBlock(toolName, errorText) — context block with truncated error text
  • Add buildSummaryBreadcrumbBlock(labels) — breadcrumb trail of summary labels
  • Add buildDataComponentBlocks(component) — section fields for flat records, JSON code block for nested, overflow to file
  • Add buildDataArtifactBlocks(artifact) — sources list for citation-type artifacts, header+description for others, overflow to file
  • Add buildCitationsBlock(citations) — deduplicated sources list, capped at 10 with "N more" suffix
  • Add isFlatRecord and findSourcesArray internal helpers

packages/agents-work-apps/src/slack/services/events/streaming.ts

  • Add toolCallId field to pendingApprovalMessages entries
  • Accumulate toolCallIdToName, toolErrors, citations, summaryLabels, richMessageCount state per stream
  • Handle tool-input-available → populate toolCallIdToName
  • Handle tool-output-denied → splice from pendingApprovalMessages
  • Handle tool-output-error → record error for footer rendering
  • Handle data-component → post blocks or upload file; increment richMessageCount
  • Handle data-artifact → accumulate citations or post/upload artifact; increment richMessageCount
  • Handle data-summary → collect label for breadcrumb
  • Append tool error blocks and summary breadcrumb to streamer.stop() footer
  • Post citations as a follow-up message after stream stop
  • Extend completion log with toolErrorCount, citationCount, richMessageCount

packages/agents-work-apps/src/slack/slack-app-manifest.json

  • Add files:write OAuth scope for file upload fallback

How to verify

  1. Trigger an agent that emits a data-component event — verify a structured Slack block appears in thread.
  2. Trigger an agent with citations (data-artifact with type: "citation") — verify a "📚 Sources" section appears after the response text.
  3. Trigger a tool that fails (tool-output-error) — verify "⚠️ ToolName · failed: …" appears in the footer alongside the agent context block.
  4. Deny a tool approval — verify the approval message is removed and no lingering state errors appear in logs.
  5. Trigger an agent with a large data payload (>2900 chars JSON) — verify a file attachment appears in the thread rather than a truncated block.

Test plan

  • Manually triggered Slack @mention flows against a dev environment agent with each event type.
  • Verified files:write scope addition does not break existing message-only flows.
  • Confirmed citation deduplication by sending two citations with the same URL — only one appears.
  • Confirmed MAX_RICH_MESSAGES cap by triggering 6+ data events in a single turn — 5 messages posted, remainder silently dropped (warn log emitted).

Handle data-component, data-artifact, data-summary, tool-output-error,
tool-output-denied, and tool-input-available stream events. Surfaces
structured data as Block Kit messages with file-upload fallback for large
payloads, deduplicates and batches citations into a post-stream footer,
and appends tool errors and summary breadcrumbs to the agent context block.

Adds files:write OAuth scope to the Slack app manifest for file upload support.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 21, 2026

⚠️ No Changeset found

Latest commit: b86ff31

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Feb 23, 2026 9:59pm
agents-manage-ui Ready Ready Preview, Comment Feb 23, 2026 9:59pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Feb 23, 2026 9:59pm

Request Review

claude[bot]

This comment was marked as resolved.

@github-actions github-actions Bot deleted a comment from claude Bot Feb 21, 2026
Removed the header block and improved the section text for tool approval messages. Updated input handling to display a maximum of 10 fields with truncation for long values. Changed button text to include emojis for better user experience.
claude[bot]

This comment was marked as resolved.

@github-actions github-actions Bot deleted a comment from claude Bot Feb 21, 2026
claude[bot]

This comment was marked as resolved.

@github-actions github-actions Bot deleted a comment from claude Bot Feb 21, 2026
@vercel vercel Bot temporarily deployed to Preview – agents-docs February 21, 2026 01:14 Inactive
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

Re-review scope: This review covers only the delta since the last automated review — commit b5acb2de9 ("fix(slack): add retryUpload helper and warn log for rich message cap"). The delta is 1 commit updating test files.

✅ Issues Addressed in This Delta

Prior Issue Resolution
🔴 blocks/index.ts:208 Existing tests will fail after buildToolApprovalBlocks refactor ✅ Tests updated to match new field-based input rendering

Test changes look good:

  • it('should include a section and actions block but no header or divider') — correctly verifies the new structure
  • it('should render input as key-value fields') — properly tests the fields array instead of JSON code block
  • it('should truncate field values longer than 80 characters with ellipsis') — matches the 80-char truncation in implementation
  • it('should not truncate field values under 80 characters') — verifies no false truncation

🕐 Pending Recommendations (2)

Issues from prior reviews that remain unresolved (not in scope for this delta, but noted for tracking):

  • 🟠 system Missing documentation for new features and OAuth scope (files:write)
  • 🟠 system Missing test coverage for new rich streaming functionality (buildDataComponentBlocks, buildDataArtifactBlocks, buildCitationsBlock, streaming event handlers)

💭 Active Consider Items (3)

These suggestions from the prior review remain open for author consideration:


💡 APPROVE WITH SUGGESTIONS

Summary: The delta commit successfully resolves the critical test failure issue. The tests now properly verify the new field-based rendering pattern introduced in the buildToolApprovalBlocks refactor.

The core functionality is ready to ship! The remaining pending items (documentation and test coverage for the new rich streaming features) can be addressed in follow-up work. The "Consider" suggestions are optional polish items left to author discretion.

Discarded (0)

No findings were discarded in this delta review.

Reviewers (1)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator 0 0 0 0 0 2 0
Total 0 0 0 0 0 2 0

Note: This was a small delta review (test updates only) handled directly by the orchestrator without spawning sub-agents.

Copy link
Copy Markdown
Collaborator

@amikofalvy amikofalvy left a comment

Choose a reason for hiding this comment

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

I have only one blocking request to not silently cap the number of rich messages and to set the original cap to a much higher value (20+ instead of 5)

"chat:write",
"chat:write.public",
"commands",
"files:write",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

with respect to pnpm setup-slack-dev

We need to make sure that this permission is included in all dev slack apps as well as prod slack apps.
This means that all i have to do to get this working with my local slack app is to re-run the setup-slack-dev script.

I suggest that we don't have divergent manifest definitions. Currently I think there is one that is statically written here and another procedurally generated one done by the setup-slack-dev script.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If you make sure to manually update all of the locations which request this new permission I will take care of merging/condensing them into a single source of truth later.

toolName: z.string(),
});

export function buildToolApprovalBlocks(params: {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There are many similarities between the two surfaces "Slack Work App" and the "Chat widget", Each of them needs to handle the agent event stream. I think it makes sense to explicitly define the set of events through a formal contract. Not blocking the merge of this PR, but I think there is something to improve in the organization of our codebase here. I will proposed a spec in a separate PR for us to review.

/**
* Retry a file upload on transient failures (5xx, AbortError) with exponential backoff.
*/
async function retryUpload<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks like a general purpose HTTP request retry function. This function is likely already defined and has unit tests. We will be able to re-use it here.

const citations: Array<{ title?: string; url?: string }> = [];
const summaryLabels: string[] = [];
let richMessageCount = 0;
const MAX_RICH_MESSAGES = 5;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it will be easy to hit this limit and it will be a confusing user experience when it occurs. Since the rich messages are silently dropped users will be left with incomplete responses that will never reach the completion state.

I think we should emit an error block kit component when this state is hit and significantly increase the original number for the max cap. Is there some reason to set it as low as 5? In my experiments with block kit, I was showing as many as 12 top level blocks and it was rendering fine on slack client.

If the number of rich elements was too large maybe the agent definition should be fixed to reduce the number of rich messages.

… test for streaming response blocks

- Updated the streaming response test to expect a section block instead of a header block.
- Replaced the retryUpload function with retryWithBackoff in the streaming service for improved error handling during file uploads.
- Increased the maximum rich messages limit from 5 to 20 in the streaming response function.
- Added the files:write scope to the Slack app manifest for file upload support.
…redundant retry file

- Imported retryWithBackoff from agents-core into nango and streaming services for improved error handling.
- Removed the now redundant retry.ts file from the slack services directory.
@github-actions github-actions Bot deleted a comment from claude Bot Feb 23, 2026
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(2) Total Issues | Risk: Low

Re-review scope: This review covers only the delta since the last automated review. The human reviewer has approved the PR. Prior findings remain tracked in Pending Recommendations below.

✅ Issues Addressed Since Last Review

Great work addressing the prior feedback:

Prior Issue Resolution
🟠 File upload lacks retry logic retryWithBackoff now wraps file uploads
🟠 Missing warning log for MAX_RICH_MESSAGES cap ✅ Warning logs added at lines 384-389 and 432-437
🟡 Error logging lacks correlation context ✅ Context now includes channel, threadTs, agentId, componentType/artifactName
🔴 Tests for buildToolApprovalBlocks ✅ Tests updated to match new field-based input rendering
🟠 MAX_RICH_MESSAGES too low ✅ Increased from 5 → 20 per human reviewer feedback
🟡 files:write scope needed in setup script ✅ Added to both slack-app-manifest.json AND scripts/setup-slack-dev.ts

New shared utility: The retryWithBackoff function was extracted from nango.ts into a shared @inkeep/agents-core utility — good refactoring to eliminate duplication.

💭 Consider (2) 💭

Inline Comments:

  • 💭 Consider: retry.ts:16 Retry-After header not respected for 429 rate limits
  • 💭 Consider: streaming.ts:359 Add label parameter for better debugging

🕐 Pending Recommendations (2)

Issues from prior reviews that remain unresolved (these are not new — linking for tracking):

  • 🟠 system Missing documentation for new features — files:write OAuth scope, rich content rendering (citations, data components, artifacts, errors), behavioral limits (20-message cap, 2900-char file upload fallback)
  • 🟠 system Missing test coverage for new functionality — retryWithBackoff utility, buildDataComponentBlocks, buildDataArtifactBlocks, buildCitationsBlock, streaming event handlers for data-component/data-artifact/data-summary/tool-output-error/tool-output-denied

💡 APPROVE WITH SUGGESTIONS

Summary: The delta changes are solid — the shared retryWithBackoff utility is well-designed with proper backoff, jitter, and error classification. The PR addresses all blocking issues from prior reviews. The human reviewer has approved.

The two "Consider" items are minor polish (Retry-After header, debug labels) and can be addressed in follow-up work. The pending recommendations (documentation, test coverage) are tracked from prior reviews and are reasonable to defer given the feature has been manually tested and approved.

Ship it! 🚀

Discarded (7)
Location Issue Reason Discarded
retry.ts Missing unit tests Already tracked in Pending Recommendations from prior review
blocks/index.ts Missing tests for buildDataComponentBlocks Already tracked in Pending Recommendations from prior review
blocks/index.ts Missing tests for buildDataArtifactBlocks Already tracked in Pending Recommendations from prior review
blocks/index.ts Missing tests for buildCitationsBlock Already tracked in Pending Recommendations from prior review
streaming.test.ts Missing integration tests for new event handlers Already tracked in Pending Recommendations from prior review
system Missing documentation Already tracked in Pending Recommendations from prior review
streaming.ts Inconsistent retry (uploads vs chat.postMessage) Intentional — file uploads are more critical for data fidelity; message posting has graceful degradation via catch
Reviewers (4)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 0 0 0 0 0 0 0
pr-review-tests 5 0 0 0 0 2 5
pr-review-docs 1 0 0 0 0 1 1
pr-review-sre 4 0 0 0 2 0 1
Total 10 0 0 0 2 3 7

Note: Most findings from test/docs reviewers were already raised in prior reviews and routed to Pending Recommendations. The delta review scope was intentionally narrow.

} catch (error) {
const isTimeout = (error as Error).name === 'AbortError';
const status = (error as { status?: number }).status;
const isRateLimit = status === 429;
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.

💭 Consider: Retry-After header not respected for 429 rate limits

Issue: The retryWithBackoff utility correctly identifies 429 rate limit responses but uses a fixed exponential backoff delay instead of respecting the Retry-After header that Slack API returns.

Why: This can lead to either: (1) retrying too soon and getting rate limited again, wasting retry attempts, or (2) waiting longer than necessary when Slack specifies a shorter delay. Slack's rate limiting documentation recommends respecting Retry-After headers.

Fix: This is a nice-to-have improvement, not blocking. If you want to implement it later:

const retryAfter = (error as { headers?: { get?: (name: string) => string | null } }).headers?.get?.('Retry-After');
const retryAfterMs = retryAfter ? (Number(retryAfter) || 0) * 1000 : 0;
const baseDelay = Math.min(500 * 2 ** (attempt - 1), maxDelayMs);
const delay = Math.max(baseDelay, retryAfterMs) + Math.random() * 100;

Refs:

});
if (overflowJson) {
const label = componentType || 'data-component';
await retryWithBackoff(() =>
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.

💭 Consider: Add label parameter for better debugging

Issue: The retryWithBackoff call doesn't pass the label option, so retry logs will show "Retrying operation after transient failure" instead of something more specific.

Why: When debugging transient failures in production, having descriptive labels helps operators quickly identify which operation is retrying.

Fix: Optional improvement:

await retryWithBackoff(
  () => slackClient.files.uploadV2({ ... }),
  { label: 'slack-file-upload' }
)

Refs:

@github-actions github-actions Bot deleted a comment from claude Bot Feb 23, 2026
anubra266 and others added 3 commits February 23, 2026 13:46
…or improved delay handling

- Updated the retryWithBackoff function to incorporate the Retry-After header, allowing for more accurate delay calculations during retries.
- Modified the streaming service to pass a label for better logging during file uploads, ensuring consistent error handling and improved traceability.
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

Re-review scope: This review covers only the delta since the last automated review — 2 commits: 5c5e735fe ("fix(retry): enhance retryWithBackoff to consider Retry-After header") and 8b8a39cdf (merge). The human reviewer has already approved.

✅ Issues Addressed in This Delta

Both "Consider" suggestions from the prior review have been implemented:

Prior Issue Resolution
💭 retry.ts:16 Retry-After header not respected for 429 rate limits ✅ Lines 19-22 now extract and respect Retry-After header, using Math.max(baseDelay, retryAfterMs) to honor Slack's requested delay
💭 streaming.ts:359 Add label parameter for better debugging ✅ Both retryWithBackoff calls now pass { label: 'slack-file-upload' } (lines 368, 423)

Implementation quality: The Retry-After implementation is clean:

  • Safely extracts header via optional chaining: error.headers?.get?.('Retry-After')
  • Converts to milliseconds: (Number(retryAfter) || 0) * 1000
  • Uses Math.max(baseDelay, retryAfterMs) to honor the larger of computed backoff or server-requested delay
  • Adds jitter to prevent thundering herd

🕐 Pending Recommendations (2)

Issues from prior reviews that remain unresolved (tracked for completeness — these are reasonable to defer given manual testing and human approval):

  • 🟠 system Missing documentation for new features — files:write OAuth scope, rich content rendering behavior, behavioral limits
  • 🟠 system Missing test coverage for new functionality — retryWithBackoff utility, block builders, streaming event handlers

💭 Active Consider Items (3)

These optional suggestions from prior reviews remain open for author consideration (not blocking):


✅ APPROVE

Summary: The delta cleanly addresses both "Consider" suggestions from the prior review. The Retry-After header implementation is well-designed and follows Slack's rate limiting best practices. Human reviewer has approved. The pending recommendations (docs, tests) are reasonable to defer — the feature has been manually tested and the core functionality is solid. Ship it! 🚀

Discarded (0)

No findings were discarded in this delta review.

Reviewers (1)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator 0 0 0 0 0 2 0
Total 0 0 0 0 0 2 0

Note: This was a minimal delta review (2 commits addressing prior "Consider" suggestions) handled directly by the orchestrator without spawning sub-agents.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

Re-review scope: This review covers only the delta since the last automated review — 1 commit: b86ff3125 ("feat(slack): add 'files:write' scope to Slack app for file upload support").

✅ Issue Addressed in This Delta

Prior Issue Resolution
🟡 slack-app-manifest.json:55 files:write scope needed in OAuth route ✅ Added to oauth.ts line 150 — now aligned across all three locations

Scope alignment complete: The files:write permission is now consistently requested in:

  1. slack-app-manifest.json (static manifest)
  2. scripts/setup-slack-dev.ts (procedural dev setup)
  3. oauth.ts:150 (runtime OAuth flow) ← this delta

This ensures existing installations will receive the new scope upon re-authorization, and new installations will have it from the start.

🕐 Pending Recommendations (2)

Issues from prior reviews that remain unresolved (tracked for completeness — these are reasonable to defer given manual testing and human approval):

  • 🟠 system Missing documentation for new features — files:write OAuth scope, rich content rendering behavior, behavioral limits
  • 🟠 system Missing test coverage for new functionality — retryWithBackoff utility, block builders, streaming event handlers

💭 Active Consider Items (3)

These optional suggestions from prior reviews remain open for author consideration (not blocking):


✅ APPROVE

Summary: The delta cleanly completes the files:write OAuth scope alignment requested by the human reviewer. All three permission definition locations are now consistent. Human reviewer has approved. The pending recommendations (docs, tests) are reasonable to defer — the feature has been manually tested and is ready to ship. 🚀

Discarded (0)

No findings were discarded in this delta review.

Reviewers (1)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
orchestrator 0 0 0 0 0 2 0
Total 0 0 0 0 0 2 0

Note: This was a minimal delta review (1-line OAuth scope addition) handled directly by the orchestrator without spawning sub-agents.

@github-actions github-actions Bot deleted a comment from claude Bot Feb 23, 2026
@anubra266 anubra266 merged commit 0056f74 into main Feb 23, 2026
11 checks passed
@anubra266 anubra266 deleted the feat/slack-rich-streaming branch February 23, 2026 22:08
inkeep Bot added a commit that referenced this pull request Feb 23, 2026
Document structured content rendering in Slack threads introduced by PR #2230:
- Data components, citations, artifacts, and summary breadcrumbs
- Tool error indicators in response footer
- File upload fallback for large payloads
inkeep Bot added a commit that referenced this pull request Feb 23, 2026
Reflects PR #2230 which added rich stream event rendering in Slack
threads — data components, artifacts, citations, and tool errors
are now displayed as structured Slack Block Kit messages.
inkeep Bot added a commit that referenced this pull request Feb 23, 2026
Adds a Tip callout after the Data Events bullet list noting that
data-component, data-artifact, data-summary, and tool events are
automatically rendered as structured Slack Block Kit messages when
using the Slack Work App. Relates to PR #2230.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants