feat(slack): render rich stream events in Slack threads#2230
feat(slack): render rich stream events in Slack threads#2230
Conversation
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.
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
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.
…d input rendering
There was a problem hiding this comment.
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 structureit('should render input as key-value fields')— properly tests the fields array instead of JSON code blockit('should truncate field values longer than 80 characters with ellipsis')— matches the 80-char truncation in implementationit('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):
- 🟠
systemMissing documentation for new features and OAuth scope (files:write) - 🟠
systemMissing 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:
- 💭
blocks/index.ts:271Parameter style differs from peer functions - 💭
blocks/index.ts:272Truncation indicator could be clearer - 💭
blocks/index.ts:283Breadcrumb lacks visual label indicating its purpose
💡 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.
amikofalvy
left a comment
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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: { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:16Retry-After header not respected for 429 rate limits - 💭 Consider:
streaming.ts:359Addlabelparameter for better debugging
🕐 Pending Recommendations (2)
Issues from prior reviews that remain unresolved (these are not new — linking for tracking):
- 🟠
systemMissing documentation for new features —files:writeOAuth scope, rich content rendering (citations, data components, artifacts, errors), behavioral limits (20-message cap, 2900-char file upload fallback) - 🟠
systemMissing test coverage for new functionality —retryWithBackoffutility,buildDataComponentBlocks,buildDataArtifactBlocks,buildCitationsBlock, streaming event handlers fordata-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; |
There was a problem hiding this comment.
💭 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(() => |
There was a problem hiding this comment.
💭 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:
…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.
…/agents into feat/slack-rich-streaming
There was a problem hiding this comment.
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") and8b8a39cdf(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):
- 🟠
systemMissing documentation for new features —files:writeOAuth scope, rich content rendering behavior, behavioral limits - 🟠
systemMissing test coverage for new functionality —retryWithBackoffutility, block builders, streaming event handlers
💭 Active Consider Items (3)
These optional suggestions from prior reviews remain open for author consideration (not blocking):
- 💭
blocks/index.ts:271Parameter style differs from peer functions - 💭
blocks/index.ts:272Truncation indicator could be clearer - 💭
blocks/index.ts:283Breadcrumb lacks visual label indicating its purpose
✅ 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.
There was a problem hiding this comment.
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:
slack-app-manifest.json(static manifest)scripts/setup-slack-dev.ts(procedural dev setup)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):
- 🟠
systemMissing documentation for new features —files:writeOAuth scope, rich content rendering behavior, behavioral limits - 🟠
systemMissing test coverage for new functionality —retryWithBackoffutility, block builders, streaming event handlers
💭 Active Consider Items (3)
These optional suggestions from prior reviews remain open for author consideration (not blocking):
- 💭
blocks/index.ts:271Parameter style differs from peer functions - 💭
blocks/index.ts:272Truncation indicator could be clearer - 💭
blocks/index.ts:283Breadcrumb lacks visual label indicating its purpose
✅ 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.
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
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.
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.
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.tshandle each event type and return Slack Block Kit payloads. The streaming loop instreaming.tsdispatches 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:toolCallIdToNamemap linkstool-input-availablecall IDs to names so thattool-output-errorevents can be labeled correctly.richMessageCountcap (5) prevents flooding a thread when agents emit many data events.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 newfiles:writeOAuth 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.
toolCallIdon pendingApprovalMessages: The existing approval tracking array was extended withtoolCallIdso 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.tsbuildToolOutputErrorBlock(toolName, errorText)— context block with truncated error textbuildSummaryBreadcrumbBlock(labels)— breadcrumb trail of summary labelsbuildDataComponentBlocks(component)— section fields for flat records, JSON code block for nested, overflow to filebuildDataArtifactBlocks(artifact)— sources list for citation-type artifacts, header+description for others, overflow to filebuildCitationsBlock(citations)— deduplicated sources list, capped at 10 with "N more" suffixisFlatRecordandfindSourcesArrayinternal helperspackages/agents-work-apps/src/slack/services/events/streaming.tstoolCallIdfield topendingApprovalMessagesentriestoolCallIdToName,toolErrors,citations,summaryLabels,richMessageCountstate per streamtool-input-available→ populatetoolCallIdToNametool-output-denied→ splice frompendingApprovalMessagestool-output-error→ record error for footer renderingdata-component→ post blocks or upload file; incrementrichMessageCountdata-artifact→ accumulate citations or post/upload artifact; incrementrichMessageCountdata-summary→ collect label for breadcrumbstreamer.stop()footertoolErrorCount,citationCount,richMessageCountpackages/agents-work-apps/src/slack/slack-app-manifest.jsonfiles:writeOAuth scope for file upload fallbackHow to verify
data-componentevent — verify a structured Slack block appears in thread.data-artifactwithtype: "citation") — verify a "📚 Sources" section appears after the response text.tool-output-error) — verify "Test plan
files:writescope addition does not break existing message-only flows.MAX_RICH_MESSAGEScap by triggering 6+ data events in a single turn — 5 messages posted, remainder silently dropped (warn log emitted).