experimental-ash 0.10.4 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +1 -1
- package/dist/docs/public/channels/README.md +26 -22
- package/dist/docs/public/channels/slack.md +57 -65
- package/dist/docs/public/connections.md +26 -25
- package/dist/docs/public/project-layout.md +1 -1
- package/dist/docs/public/session-context.md +14 -12
- package/dist/docs/public/typescript-api.md +5 -4
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/defaults.d.ts +28 -0
- package/dist/src/public/channels/slack/defaults.js +223 -0
- package/dist/src/public/channels/slack/index.d.ts +1 -2
- package/dist/src/public/channels/slack/index.js +0 -1
- package/dist/src/public/channels/slack/slackChannel.d.ts +37 -21
- package/dist/src/public/channels/slack/slackChannel.js +30 -42
- package/package.json +1 -1
- package/dist/src/public/channels/slack/slack.d.ts +0 -20
- package/dist/src/public/channels/slack/slack.js +0 -200
|
@@ -66,12 +66,13 @@ Ash also exports lower-level runtime primitives such as `createToolLoopHarness(.
|
|
|
66
66
|
|
|
67
67
|
Channel and Slack types exported from `experimental-ash/channels/slack`:
|
|
68
68
|
|
|
69
|
-
- `
|
|
70
|
-
|
|
71
|
-
- `SlackChannelConfig` - config type for `slackChannel`
|
|
69
|
+
- `slackChannel` - Slack channel factory; zero-config by default, accepts a `SlackChannelConfig`
|
|
70
|
+
to override `onMention`, individual event handlers, `onInteraction`, etc.
|
|
71
|
+
- `SlackChannelConfig` - config type for `slackChannel(...)`
|
|
72
72
|
- `SlackContext` - context type for Slack event handlers (`thread`, `slack`)
|
|
73
73
|
- `SlackApiHandle` - Slack API handle with `channelId`, `threadTs`, `request()`
|
|
74
74
|
- `SlackInteractionAction` - action type for `onInteraction`
|
|
75
|
+
- `SlackMentionResult` - return type of `onMention` (`{ auth } | null`)
|
|
75
76
|
- `Thread`, `Message`, `Card`, `Button`, `Actions`, etc. - Chat SDK types for Slack rendering
|
|
76
77
|
|
|
77
78
|
Channel types exported from `experimental-ash/channels`:
|
|
@@ -114,7 +115,7 @@ import { defineAgent } from "experimental-ash";
|
|
|
114
115
|
import { defineChannel, POST, GET } from "experimental-ash/channels";
|
|
115
116
|
import { ashChannel } from "experimental-ash/channels/ash";
|
|
116
117
|
import { vercelOidc } from "experimental-ash/channels/auth";
|
|
117
|
-
import {
|
|
118
|
+
import { slackChannel } from "experimental-ash/channels/slack";
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
Inside a route handler, the helpers object exposes:
|
|
@@ -6,7 +6,7 @@ import { ASH_PACKAGE_NAME } from "#package-name.js";
|
|
|
6
6
|
let cachedPackageInfo;
|
|
7
7
|
// The package build stamps the published version into `dist` so bundled
|
|
8
8
|
// deployments can still report package metadata without resolving package.json.
|
|
9
|
-
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.
|
|
9
|
+
const BUNDLED_FALLBACK_PACKAGE_VERSION = "0.11.1";
|
|
10
10
|
const BUNDLED_FALLBACK_PACKAGE_VERSION_PLACEHOLDER = "__ASH_PACKAGE_VERSION_PLACEHOLDER__";
|
|
11
11
|
const WORKFLOW_MODULE_ALIASES = {
|
|
12
12
|
"workflow/api": "src/compiled/@workflow/core/runtime.js",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "#channel/types.js";
|
|
2
|
+
import type { Message } from "#compiled/chat/index.js";
|
|
3
|
+
import type { SlackChannelEvents, SlackContext, SlackMentionResult } from "#public/channels/slack/slackChannel.js";
|
|
4
|
+
/**
|
|
5
|
+
* Workspace-scoped projection of the Slack actor that produced
|
|
6
|
+
* `message`. Used by {@link defaultOnMention} to derive a
|
|
7
|
+
* {@link SessionAuthContext} when the customer hasn't supplied their
|
|
8
|
+
* own `onMention`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function defaultSlackAuth(message: Message, ctx: SlackContext): SessionAuthContext | null;
|
|
11
|
+
/**
|
|
12
|
+
* Default `onMention` — derives auth from the Slack actor and posts a
|
|
13
|
+
* `"Thinking…"` typing indicator before the workflow runtime starts.
|
|
14
|
+
*/
|
|
15
|
+
export declare function defaultOnMention(ctx: SlackContext, message: Message): Promise<SlackMentionResult>;
|
|
16
|
+
/**
|
|
17
|
+
* Default `input.requested` handler — renders each pending HITL
|
|
18
|
+
* request as Slack `block_actions`. Buttons by default; radio for
|
|
19
|
+
* ≤6-option select requests; static_select for >6-option select
|
|
20
|
+
* requests. Override by declaring `events["input.requested"]`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function defaultInputRequestedHandler(): NonNullable<SlackChannelEvents["input.requested"]>;
|
|
23
|
+
/**
|
|
24
|
+
* Built-in Slack event handlers — typing indicators, error replies,
|
|
25
|
+
* and the connection-authorization status flow. Each is overridable
|
|
26
|
+
* per-event by passing the same key under `slackChannel({ events })`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const defaultEvents: SlackChannelEvents;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createLogger, extractErrorId, formatErrorHint } from "#internal/logging.js";
|
|
2
|
+
import { decodeThreadId } from "#public/channels/slack/api.js";
|
|
3
|
+
import { buildAuthCompletedText, buildAuthEphemeralBlocks, buildAuthRequiredPublicText, formatConnectionDisplayName, } from "#public/channels/slack/connections.js";
|
|
4
|
+
import { renderInputRequestBlocks } from "#public/channels/slack/hitl.js";
|
|
5
|
+
import { truncateTypingStatus } from "#public/channels/slack/limits.js";
|
|
6
|
+
const log = createLogger("slack.defaults");
|
|
7
|
+
function readTeamId(message) {
|
|
8
|
+
const raw = message.raw;
|
|
9
|
+
const teamId = raw?.team_id ?? raw?.team;
|
|
10
|
+
return typeof teamId === "string" && teamId.length > 0 ? teamId : undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Workspace-scoped projection of the Slack actor that produced
|
|
14
|
+
* `message`. Used by {@link defaultOnMention} to derive a
|
|
15
|
+
* {@link SessionAuthContext} when the customer hasn't supplied their
|
|
16
|
+
* own `onMention`.
|
|
17
|
+
*/
|
|
18
|
+
export function defaultSlackAuth(message, ctx) {
|
|
19
|
+
const author = message.author;
|
|
20
|
+
if (!author)
|
|
21
|
+
return null;
|
|
22
|
+
const teamId = readTeamId(message);
|
|
23
|
+
const isBot = author.isBot === true;
|
|
24
|
+
const userId = String(author.userId);
|
|
25
|
+
const principalId = teamId
|
|
26
|
+
? isBot
|
|
27
|
+
? `slack:${teamId}:bot:${userId}`
|
|
28
|
+
: `slack:${teamId}:${userId}`
|
|
29
|
+
: isBot
|
|
30
|
+
? `slack:bot:${userId}`
|
|
31
|
+
: `slack:${userId}`;
|
|
32
|
+
const attributes = {
|
|
33
|
+
author_type: isBot ? "bot" : "user",
|
|
34
|
+
channel_id: ctx.slack.channelId,
|
|
35
|
+
thread_ts: ctx.slack.threadTs,
|
|
36
|
+
user_id: userId,
|
|
37
|
+
};
|
|
38
|
+
if (typeof author.userName === "string" && author.userName.length > 0) {
|
|
39
|
+
attributes.user_name = author.userName;
|
|
40
|
+
}
|
|
41
|
+
if (typeof author.fullName === "string" && author.fullName.length > 0) {
|
|
42
|
+
attributes.full_name = author.fullName;
|
|
43
|
+
}
|
|
44
|
+
if (teamId !== undefined) {
|
|
45
|
+
attributes.team_id = teamId;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
attributes,
|
|
49
|
+
authenticator: "slack-webhook",
|
|
50
|
+
issuer: teamId !== undefined ? `slack:${teamId}` : "slack",
|
|
51
|
+
principalId,
|
|
52
|
+
principalType: isBot ? "service" : "user",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Default `onMention` — derives auth from the Slack actor and posts a
|
|
57
|
+
* `"Thinking…"` typing indicator before the workflow runtime starts.
|
|
58
|
+
*/
|
|
59
|
+
export async function defaultOnMention(ctx, message) {
|
|
60
|
+
await ctx.thread.startTyping("Thinking...");
|
|
61
|
+
return { auth: defaultSlackAuth(message, ctx) };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Reads the first non-empty line of a model-emitted message. The
|
|
65
|
+
* default `actions.requested` handler uses this to surface the
|
|
66
|
+
* model's own pre-tool-call narration as the typing indicator.
|
|
67
|
+
*/
|
|
68
|
+
function firstNonEmptyLine(text) {
|
|
69
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (trimmed.length > 0)
|
|
72
|
+
return trimmed;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Default `input.requested` handler — renders each pending HITL
|
|
78
|
+
* request as Slack `block_actions`. Buttons by default; radio for
|
|
79
|
+
* ≤6-option select requests; static_select for >6-option select
|
|
80
|
+
* requests. Override by declaring `events["input.requested"]`.
|
|
81
|
+
*/
|
|
82
|
+
export function defaultInputRequestedHandler() {
|
|
83
|
+
return async (data, ctx) => {
|
|
84
|
+
if (data.requests.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const decoded = decodeThreadId(ctx.thread.id ?? "");
|
|
87
|
+
await ctx.slack.request("chat.postMessage", {
|
|
88
|
+
channel: decoded.channelId,
|
|
89
|
+
thread_ts: decoded.threadTs,
|
|
90
|
+
blocks: data.requests.flatMap(renderInputRequestBlocks),
|
|
91
|
+
text: data.requests.map((r) => r.prompt).join("\n"),
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Built-in Slack event handlers — typing indicators, error replies,
|
|
97
|
+
* and the connection-authorization status flow. Each is overridable
|
|
98
|
+
* per-event by passing the same key under `slackChannel({ events })`.
|
|
99
|
+
*/
|
|
100
|
+
export const defaultEvents = {
|
|
101
|
+
async "turn.started"(_event, ctx) {
|
|
102
|
+
ctx.state.pendingToolCallMessage = null;
|
|
103
|
+
await ctx.thread.startTyping("Working...");
|
|
104
|
+
},
|
|
105
|
+
async "actions.requested"(event, ctx) {
|
|
106
|
+
const buffered = ctx.state.pendingToolCallMessage;
|
|
107
|
+
ctx.state.pendingToolCallMessage = null;
|
|
108
|
+
if (buffered) {
|
|
109
|
+
await ctx.thread.startTyping(truncateTypingStatus(buffered));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const labels = event.actions.map((a) => (a.kind === "tool-call" ? a.toolName : a.kind));
|
|
113
|
+
await ctx.thread.startTyping(truncateTypingStatus(`Running ${labels.join(", ")}...`));
|
|
114
|
+
},
|
|
115
|
+
async "message.completed"(event, ctx) {
|
|
116
|
+
if (event.finishReason === "tool-calls") {
|
|
117
|
+
// Buffer the model's prose so `actions.requested` can surface
|
|
118
|
+
// it as the typing indicator. Don't post — there's no
|
|
119
|
+
// user-visible answer yet, just narration before a tool call.
|
|
120
|
+
ctx.state.pendingToolCallMessage = event.message
|
|
121
|
+
? (firstNonEmptyLine(event.message) ?? null)
|
|
122
|
+
: null;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
ctx.state.pendingToolCallMessage = null;
|
|
126
|
+
if (event.message)
|
|
127
|
+
await ctx.thread.post({ markdown: event.message });
|
|
128
|
+
},
|
|
129
|
+
async "turn.failed"(event, ctx) {
|
|
130
|
+
const hint = formatErrorHint(event);
|
|
131
|
+
const errorId = extractErrorId(event.details);
|
|
132
|
+
await ctx.thread.post({
|
|
133
|
+
markdown: [
|
|
134
|
+
`I hit an error while handling your request${hint}.`,
|
|
135
|
+
"",
|
|
136
|
+
"Please try again, rephrase, or reach out if it keeps failing.",
|
|
137
|
+
...(errorId ? ["", `_Error id: \`${errorId}\`_`] : []),
|
|
138
|
+
].join("\n"),
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
async "session.failed"(event, ctx) {
|
|
142
|
+
const hint = formatErrorHint(event);
|
|
143
|
+
const errorId = extractErrorId(event.details);
|
|
144
|
+
await ctx.thread.post({
|
|
145
|
+
markdown: [
|
|
146
|
+
`This session couldn't recover from an error${hint}.`,
|
|
147
|
+
"",
|
|
148
|
+
"Start a new thread to continue — I can't pick this one back up.",
|
|
149
|
+
...(errorId ? ["", `_Error id: \`${errorId}\`_`] : []),
|
|
150
|
+
].join("\n"),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
async "connection.authorization_required"(event, ctx) {
|
|
154
|
+
const displayName = formatConnectionDisplayName(event.connectionName);
|
|
155
|
+
const triggeringUserId = ctx.state.triggeringUserId ?? null;
|
|
156
|
+
const challengeUrl = event.authorization?.url;
|
|
157
|
+
if (triggeringUserId && challengeUrl) {
|
|
158
|
+
try {
|
|
159
|
+
await ctx.slack.request("chat.postEphemeral", {
|
|
160
|
+
channel: ctx.slack.channelId,
|
|
161
|
+
user: triggeringUserId,
|
|
162
|
+
thread_ts: ctx.slack.threadTs,
|
|
163
|
+
blocks: buildAuthEphemeralBlocks({ displayName, url: challengeUrl }),
|
|
164
|
+
text: `Sign in with ${displayName}: ${challengeUrl}`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
log.error("Slack auth ephemeral delivery failed", {
|
|
169
|
+
connectionName: event.connectionName,
|
|
170
|
+
error,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const publicText = buildAuthRequiredPublicText({
|
|
175
|
+
displayName,
|
|
176
|
+
hasUser: triggeringUserId !== null,
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
const sent = await ctx.thread.post({ markdown: publicText });
|
|
180
|
+
const sentId = sent && typeof sent === "object" && "id" in sent ? sent.id : undefined;
|
|
181
|
+
if (typeof sentId === "string") {
|
|
182
|
+
ctx.state.pendingAuthMessageTs = {
|
|
183
|
+
...ctx.state.pendingAuthMessageTs,
|
|
184
|
+
[event.connectionName]: sentId,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
log.error("Slack auth public message delivery failed", {
|
|
190
|
+
connectionName: event.connectionName,
|
|
191
|
+
error,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
async "connection.authorization_completed"(event, ctx) {
|
|
196
|
+
const pending = ctx.state.pendingAuthMessageTs ?? {};
|
|
197
|
+
const ts = pending[event.connectionName];
|
|
198
|
+
if (ts === undefined)
|
|
199
|
+
return;
|
|
200
|
+
const displayName = formatConnectionDisplayName(event.connectionName);
|
|
201
|
+
const text = buildAuthCompletedText({
|
|
202
|
+
displayName,
|
|
203
|
+
outcome: event.outcome,
|
|
204
|
+
reason: event.reason,
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
await ctx.slack.request("chat.update", {
|
|
208
|
+
channel: ctx.slack.channelId,
|
|
209
|
+
ts,
|
|
210
|
+
text,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
log.error("Slack auth status edit failed", {
|
|
215
|
+
connectionName: event.connectionName,
|
|
216
|
+
error,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
const next = { ...pending };
|
|
220
|
+
delete next[event.connectionName];
|
|
221
|
+
ctx.state.pendingAuthMessageTs = next;
|
|
222
|
+
},
|
|
223
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannel, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackInteractionAction, type SlackReceiveArgs, type SlackStateAdapter, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
|
|
1
|
+
export { slackChannel, type SlackApiHandle, type SlackApiResponse, type SlackChannel, type SlackChannelConfig, type SlackChannelEvents, type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackInteractionAction, type SlackMentionResult, type SlackMentionResultOrPromise, type SlackReceiveArgs, type SlackStateAdapter, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.js";
|
|
3
2
|
export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
|
|
4
3
|
export type { AdapterPostableMessage, Attachment, Author, CardElement, FileUpload, Message, PostableMessage, SentMessage, Thread, } from "#compiled/chat/index.js";
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export { slack } from "#public/channels/slack/slack.js";
|
|
2
1
|
export { slackChannel, } from "#public/channels/slack/slackChannel.js";
|
|
3
2
|
export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "#channel/types.js";
|
|
1
2
|
import { type SlackBotToken } from "#compiled/@chat-adapter/slack/index.js";
|
|
2
3
|
import { type Message, type SerializedThread, type StateAdapter, type Thread } from "#compiled/chat/index.js";
|
|
3
4
|
import type { HandleMessageStreamEvent } from "#protocol/message.js";
|
|
@@ -9,7 +10,7 @@ type EventData<T extends HandleMessageStreamEvent["type"]> = Extract<HandleMessa
|
|
|
9
10
|
data: infer D;
|
|
10
11
|
} ? D : undefined;
|
|
11
12
|
/**
|
|
12
|
-
* Pre-dispatch Slack context — handed to `
|
|
13
|
+
* Pre-dispatch Slack context — handed to `onMention` and
|
|
13
14
|
* `onInteraction`. These hooks run on the inbound webhook side, before
|
|
14
15
|
* the runtime hydrates any session state, so `state` is intentionally
|
|
15
16
|
* absent here.
|
|
@@ -124,10 +125,15 @@ export interface SlackInteractionAction {
|
|
|
124
125
|
*/
|
|
125
126
|
readonly label?: string;
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Result of an `onMention` callback. Return `{ auth }` (auth may be
|
|
130
|
+
* `null`) to dispatch a turn with that session auth context, or `null`
|
|
131
|
+
* to silently drop the mention.
|
|
132
|
+
*/
|
|
133
|
+
export type SlackMentionResult = {
|
|
134
|
+
auth: SessionAuthContext | null;
|
|
129
135
|
} | null;
|
|
130
|
-
export type
|
|
136
|
+
export type SlackMentionResultOrPromise = SlackMentionResult | Promise<SlackMentionResult>;
|
|
131
137
|
export interface SlackChannelEvents {
|
|
132
138
|
readonly "turn.started"?: SlackEventHandler<"turn.started">;
|
|
133
139
|
readonly "actions.requested"?: SlackEventHandler<"actions.requested">;
|
|
@@ -170,25 +176,25 @@ export interface SlackChannelConfig {
|
|
|
170
176
|
*/
|
|
171
177
|
readonly uploadPolicy?: Partial<UploadPolicy>;
|
|
172
178
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* side
|
|
178
|
-
*/
|
|
179
|
-
run?(ctx: SlackContext, message: Message): SlackRunResultOrPromise;
|
|
180
|
-
/**
|
|
181
|
-
* Free-form pre-dispatch hook invoked after `run()` accepts but
|
|
182
|
-
* before the framework enqueues the turn. Use it for side effects
|
|
183
|
-
* that should fire on the inbound webhook side — for example,
|
|
184
|
-
* starting a typing indicator so the user sees feedback before the
|
|
185
|
-
* workflow runtime cold-starts.
|
|
179
|
+
* Invoked the moment a Slack `app_mention` arrives, before the
|
|
180
|
+
* framework dispatches a turn. Decides whether to dispatch and
|
|
181
|
+
* with what auth, and may perform pre-dispatch side effects (e.g.
|
|
182
|
+
* `ctx.thread.startTyping("Thinking…")`) on the inbound webhook
|
|
183
|
+
* side before the workflow runtime cold-starts.
|
|
186
184
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
185
|
+
* Return `{ auth }` to dispatch with that session auth context, or
|
|
186
|
+
* `null` to silently drop the mention. May be sync or async; the
|
|
187
|
+
* framework awaits the result before dispatching.
|
|
188
|
+
*
|
|
189
|
+
* Thrown errors are caught and logged, and the mention is dropped
|
|
190
|
+
* (no dispatch). Wrap best-effort side effects in `try/catch` if
|
|
191
|
+
* you want them to be non-fatal.
|
|
192
|
+
*
|
|
193
|
+
* Defaults to a workspace-scoped auth derivation that posts a
|
|
194
|
+
* `"Thinking…"` typing indicator. Replacing this option fully
|
|
195
|
+
* replaces both behaviors.
|
|
190
196
|
*/
|
|
191
|
-
onMention?(ctx: SlackContext, message: Message):
|
|
197
|
+
onMention?(ctx: SlackContext, message: Message): SlackMentionResultOrPromise;
|
|
192
198
|
/**
|
|
193
199
|
* Handler for Slack `block_actions` interactive callbacks (button
|
|
194
200
|
* clicks, select changes, etc.) that are **not** consumed by the
|
|
@@ -222,5 +228,15 @@ export interface SlackChannelConfig {
|
|
|
222
228
|
*/
|
|
223
229
|
export interface SlackChannel extends Channel<SlackChannelState> {
|
|
224
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Slack channel factory. Wires up the Slack webhook route, mention
|
|
233
|
+
* dispatch, interaction handling, and a baseline set of typing /
|
|
234
|
+
* error / connection-auth event handlers.
|
|
235
|
+
*
|
|
236
|
+
* Defaults apply per field — pass `onMention` to fully replace the
|
|
237
|
+
* default mention pipeline (auth derivation + `"Thinking…"` typing),
|
|
238
|
+
* or pass an `events[type]` handler to replace only that one event.
|
|
239
|
+
* Fields you don't supply keep their defaults.
|
|
240
|
+
*/
|
|
225
241
|
export declare function slackChannel(config?: SlackChannelConfig): SlackChannel;
|
|
226
242
|
export {};
|
|
@@ -4,7 +4,7 @@ import { ThreadImpl, } from "#compiled/chat/index.js";
|
|
|
4
4
|
import { createLogger } from "#internal/logging.js";
|
|
5
5
|
import { buildSlackApiHandle, decodeThreadId } from "#public/channels/slack/api.js";
|
|
6
6
|
import { buildSlackTurnMessage, collectInboundFileParts, createSlackFetchFile, } from "#public/channels/slack/attachments.js";
|
|
7
|
-
import {
|
|
7
|
+
import { defaultEvents, defaultInputRequestedHandler, defaultOnMention, } from "#public/channels/slack/defaults.js";
|
|
8
8
|
import { prependSlackContext, renderInboundText, } from "#public/channels/slack/inbound.js";
|
|
9
9
|
import { handleInteractionPost } from "#public/channels/slack/interactions.js";
|
|
10
10
|
import { mergeUploadPolicy } from "#public/channels/upload-policy.js";
|
|
@@ -51,25 +51,6 @@ function rebuildSlackContext(state, credentials, stateAdapter) {
|
|
|
51
51
|
state,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Default `input.requested` handler — renders each pending HITL
|
|
56
|
-
* request as Slack `block_actions`. Buttons by default; radio for
|
|
57
|
-
* ≤6-option select requests; static_select for >6-option select
|
|
58
|
-
* requests. Override by declaring `events["input.requested"]`.
|
|
59
|
-
*/
|
|
60
|
-
function defaultInputRequestedHandler() {
|
|
61
|
-
return async (data, ctx) => {
|
|
62
|
-
if (data.requests.length === 0)
|
|
63
|
-
return;
|
|
64
|
-
const decoded = decodeThreadId(ctx.thread.id ?? "");
|
|
65
|
-
await ctx.slack.request("chat.postMessage", {
|
|
66
|
-
channel: decoded.channelId,
|
|
67
|
-
thread_ts: decoded.threadTs,
|
|
68
|
-
blocks: data.requests.flatMap(renderInputRequestBlocks),
|
|
69
|
-
text: data.requests.map((r) => r.prompt).join("\n"),
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
54
|
/**
|
|
74
55
|
* Build the once-registered `onNewMention` listener for a `Chat`
|
|
75
56
|
* instance. The chat SDK defers handler invocation through the
|
|
@@ -84,33 +65,29 @@ function defaultInputRequestedHandler() {
|
|
|
84
65
|
* lambda may terminate.
|
|
85
66
|
*/
|
|
86
67
|
function buildMentionListener(config, uploadPolicy, getSend) {
|
|
68
|
+
const onMention = config.onMention ?? defaultOnMention;
|
|
87
69
|
return async (thread, message) => {
|
|
88
70
|
const raw = message.raw;
|
|
89
|
-
// Slack delivers `app_mention` and `message.channels` for the same
|
|
90
|
-
// utterance; only the former is the listener's job.
|
|
91
|
-
if (raw?.type !== "app_mention")
|
|
92
|
-
return;
|
|
93
71
|
const send = getSend();
|
|
94
72
|
if (!send) {
|
|
95
73
|
log.warn("slack mention received before any request captured send");
|
|
96
74
|
return;
|
|
97
75
|
}
|
|
98
|
-
const teamId = raw
|
|
76
|
+
const teamId = raw?.team_id ?? raw?.team;
|
|
99
77
|
const slackCtx = {
|
|
100
78
|
thread,
|
|
101
79
|
slack: buildSlackApiHandle(thread, config.credentials?.botToken, teamId),
|
|
102
80
|
};
|
|
103
|
-
|
|
104
|
-
|
|
81
|
+
let mentionResult;
|
|
82
|
+
try {
|
|
83
|
+
mentionResult = await onMention(slackCtx, message);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
log.error("onMention handler failed", { error });
|
|
105
87
|
return;
|
|
106
|
-
if (config.onMention) {
|
|
107
|
-
try {
|
|
108
|
-
await config.onMention(slackCtx, message);
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
log.error("onMention handler failed", { error });
|
|
112
|
-
}
|
|
113
88
|
}
|
|
89
|
+
if (mentionResult === null)
|
|
90
|
+
return;
|
|
114
91
|
const decoded = decodeThreadId(thread.id ?? "");
|
|
115
92
|
const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
|
|
116
93
|
const renderedText = renderInboundText(message);
|
|
@@ -133,7 +110,7 @@ function buildMentionListener(config, uploadPolicy, getSend) {
|
|
|
133
110
|
: baseMessage;
|
|
134
111
|
try {
|
|
135
112
|
await send(turnMessage, {
|
|
136
|
-
auth:
|
|
113
|
+
auth: mentionResult.auth,
|
|
137
114
|
continuationToken,
|
|
138
115
|
state: {
|
|
139
116
|
serializedThread: thread.toJSON(),
|
|
@@ -147,11 +124,25 @@ function buildMentionListener(config, uploadPolicy, getSend) {
|
|
|
147
124
|
}
|
|
148
125
|
};
|
|
149
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Slack channel factory. Wires up the Slack webhook route, mention
|
|
129
|
+
* dispatch, interaction handling, and a baseline set of typing /
|
|
130
|
+
* error / connection-auth event handlers.
|
|
131
|
+
*
|
|
132
|
+
* Defaults apply per field — pass `onMention` to fully replace the
|
|
133
|
+
* default mention pipeline (auth derivation + `"Thinking…"` typing),
|
|
134
|
+
* or pass an `events[type]` handler to replace only that one event.
|
|
135
|
+
* Fields you don't supply keep their defaults.
|
|
136
|
+
*/
|
|
150
137
|
export function slackChannel(config = {}) {
|
|
151
138
|
const uploadPolicy = mergeUploadPolicy(config.uploadPolicy);
|
|
152
139
|
const slackFetchFile = createSlackFetchFile({ botToken: config.credentials?.botToken });
|
|
153
140
|
const stateAdapter = config.stateAdapter ?? createMemoryState();
|
|
154
|
-
const
|
|
141
|
+
const mergedEvents = {
|
|
142
|
+
...defaultEvents,
|
|
143
|
+
...config.events,
|
|
144
|
+
"input.requested": config.events?.["input.requested"] ?? defaultInputRequestedHandler(),
|
|
145
|
+
};
|
|
155
146
|
// The chat SDK defers mention handler invocation past the route
|
|
156
147
|
// handler returning, so the listener can't read `send` off the
|
|
157
148
|
// current request. Capture it on the first request and reuse it —
|
|
@@ -166,7 +157,7 @@ export function slackChannel(config = {}) {
|
|
|
166
157
|
chatPromise = (async () => {
|
|
167
158
|
const { botToken, signingSecret, webhookVerifier } = resolveSlackAdapterCredentials(config.credentials);
|
|
168
159
|
if (!botToken) {
|
|
169
|
-
throw new Error("slackChannel requires a bot token. Pass credentials.botToken or set SLACK_BOT_TOKEN.");
|
|
160
|
+
throw new Error("slackChannel() requires a bot token. Pass credentials.botToken or set SLACK_BOT_TOKEN.");
|
|
170
161
|
}
|
|
171
162
|
const [slackModule, chatModule] = await Promise.all([
|
|
172
163
|
import("#compiled/@chat-adapter/slack/index.js"),
|
|
@@ -226,7 +217,7 @@ export function slackChannel(config = {}) {
|
|
|
226
217
|
await getChat();
|
|
227
218
|
const channelId = input.args.channelId;
|
|
228
219
|
if (!channelId) {
|
|
229
|
-
throw new Error("slackChannel.receive requires args.channelId.");
|
|
220
|
+
throw new Error("slackChannel().receive requires args.channelId.");
|
|
230
221
|
}
|
|
231
222
|
const chatModule = await import("#compiled/chat/index.js");
|
|
232
223
|
const thread = new chatModule.ThreadImpl({
|
|
@@ -245,9 +236,6 @@ export function slackChannel(config = {}) {
|
|
|
245
236
|
},
|
|
246
237
|
});
|
|
247
238
|
},
|
|
248
|
-
events:
|
|
249
|
-
...config.events,
|
|
250
|
-
"input.requested": inputHandler,
|
|
251
|
-
},
|
|
239
|
+
events: mergedEvents,
|
|
252
240
|
});
|
|
253
241
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { SessionAuthContext } from "#channel/types.js";
|
|
2
|
-
import type { Channel } from "#public/definitions/defineChannel.js";
|
|
3
|
-
import { type SlackChannelCredentials, type SlackChannelState, type SlackContext, type SlackEventContext, type SlackStateAdapter } from "#public/channels/slack/slackChannel.js";
|
|
4
|
-
export interface SlackOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Maps a verified inbound Slack mention to a {@link SessionAuthContext}.
|
|
7
|
-
* Receives the chat SDK `Message` plus a {@link SlackContext} carrying
|
|
8
|
-
* the live `thread` and `slack` API handle so policies can read
|
|
9
|
-
* channel/thread metadata without re-parsing `message.raw`.
|
|
10
|
-
*
|
|
11
|
-
* Defaults to a workspace-scoped projection of the Slack actor; return
|
|
12
|
-
* `null` to silently drop the mention.
|
|
13
|
-
*/
|
|
14
|
-
readonly auth?: (message: import("#compiled/chat/index.js").Message, ctx: SlackContext) => SessionAuthContext | null;
|
|
15
|
-
readonly credentials?: SlackChannelCredentials;
|
|
16
|
-
readonly botName?: string;
|
|
17
|
-
readonly stateAdapter?: SlackStateAdapter;
|
|
18
|
-
}
|
|
19
|
-
export declare function slack(options?: SlackOptions): Channel<SlackChannelState>;
|
|
20
|
-
export type { SlackEventContext };
|