experimental-ash 0.10.3 → 0.11.0
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 +69 -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/harness/input-requests.js +46 -18
- 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 +29 -37
- 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
|
@@ -226,30 +226,58 @@ function buildToolResponseParts(batch, responses) {
|
|
|
226
226
|
const responseMap = new Map(responses.map((r) => [r.requestId, r]));
|
|
227
227
|
const parts = [];
|
|
228
228
|
for (const request of batch.requests) {
|
|
229
|
-
parts.push(
|
|
229
|
+
parts.push(...buildToolResponsePartsForRequest(request, responseMap.get(request.requestId)));
|
|
230
230
|
}
|
|
231
231
|
return parts;
|
|
232
232
|
}
|
|
233
|
-
function
|
|
233
|
+
function buildToolResponsePartsForRequest(request, response) {
|
|
234
234
|
if (isApprovalRequest(request)) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
const approved = response?.optionId === "approve";
|
|
236
|
+
const reason = response === undefined ? IGNORED_INPUT_REASON : undefined;
|
|
237
|
+
const parts = [
|
|
238
|
+
{
|
|
239
|
+
approvalId: request.requestId,
|
|
240
|
+
approved,
|
|
241
|
+
reason,
|
|
242
|
+
type: "tool-approval-response",
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
/*
|
|
246
|
+
* On denial (explicit "deny" or auto-deny when the user continues
|
|
247
|
+
* without responding), splice in the matching `execution-denied`
|
|
248
|
+
* tool-result. AI SDK's `streamText` synthesizes this for the
|
|
249
|
+
* current turn's `initialResponseMessages`, but that synthesis is
|
|
250
|
+
* gated on the input messages' last entry being a tool message —
|
|
251
|
+
* on subsequent turns (when a new user message is the tail of
|
|
252
|
+
* history) the synthesis is skipped, and the persisted
|
|
253
|
+
* `tool-approval-response` is stripped during provider prompt
|
|
254
|
+
* conversion. Without an own `tool-result` in history, the prior
|
|
255
|
+
* `tool_use` block replays unmatched and some providers reject
|
|
256
|
+
* the request with 400.
|
|
257
|
+
*/
|
|
258
|
+
if (!approved) {
|
|
259
|
+
parts.push({
|
|
260
|
+
output: { type: "execution-denied", reason },
|
|
261
|
+
toolCallId: request.action.callId,
|
|
262
|
+
toolName: request.action.toolName,
|
|
263
|
+
type: "tool-result",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return parts;
|
|
241
267
|
}
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
output: {
|
|
271
|
+
type: "json",
|
|
272
|
+
value: response !== undefined
|
|
273
|
+
? { optionId: response.optionId, text: response.text, status: "answered" }
|
|
274
|
+
: { status: "ignored" },
|
|
275
|
+
},
|
|
276
|
+
toolCallId: request.action.callId,
|
|
277
|
+
toolName: request.action.toolName,
|
|
278
|
+
type: "tool-result",
|
|
248
279
|
},
|
|
249
|
-
|
|
250
|
-
toolName: request.action.toolName,
|
|
251
|
-
type: "tool-result",
|
|
252
|
-
};
|
|
280
|
+
];
|
|
253
281
|
}
|
|
254
282
|
function isApprovalRequest(request) {
|
|
255
283
|
return (request.options?.length === 2 &&
|
|
@@ -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.0";
|
|
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,6 +65,7 @@ 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
71
|
// Slack delivers `app_mention` and `message.channels` for the same
|
|
@@ -100,17 +82,16 @@ function buildMentionListener(config, uploadPolicy, getSend) {
|
|
|
100
82
|
thread,
|
|
101
83
|
slack: buildSlackApiHandle(thread, config.credentials?.botToken, teamId),
|
|
102
84
|
};
|
|
103
|
-
|
|
104
|
-
|
|
85
|
+
let mentionResult;
|
|
86
|
+
try {
|
|
87
|
+
mentionResult = await onMention(slackCtx, message);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
log.error("onMention handler failed", { error });
|
|
105
91
|
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
92
|
}
|
|
93
|
+
if (mentionResult === null)
|
|
94
|
+
return;
|
|
114
95
|
const decoded = decodeThreadId(thread.id ?? "");
|
|
115
96
|
const continuationToken = `slack:${decoded.channelId}:${decoded.threadTs}`;
|
|
116
97
|
const renderedText = renderInboundText(message);
|
|
@@ -133,7 +114,7 @@ function buildMentionListener(config, uploadPolicy, getSend) {
|
|
|
133
114
|
: baseMessage;
|
|
134
115
|
try {
|
|
135
116
|
await send(turnMessage, {
|
|
136
|
-
auth:
|
|
117
|
+
auth: mentionResult.auth,
|
|
137
118
|
continuationToken,
|
|
138
119
|
state: {
|
|
139
120
|
serializedThread: thread.toJSON(),
|
|
@@ -147,11 +128,25 @@ function buildMentionListener(config, uploadPolicy, getSend) {
|
|
|
147
128
|
}
|
|
148
129
|
};
|
|
149
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Slack channel factory. Wires up the Slack webhook route, mention
|
|
133
|
+
* dispatch, interaction handling, and a baseline set of typing /
|
|
134
|
+
* error / connection-auth event handlers.
|
|
135
|
+
*
|
|
136
|
+
* Defaults apply per field — pass `onMention` to fully replace the
|
|
137
|
+
* default mention pipeline (auth derivation + `"Thinking…"` typing),
|
|
138
|
+
* or pass an `events[type]` handler to replace only that one event.
|
|
139
|
+
* Fields you don't supply keep their defaults.
|
|
140
|
+
*/
|
|
150
141
|
export function slackChannel(config = {}) {
|
|
151
142
|
const uploadPolicy = mergeUploadPolicy(config.uploadPolicy);
|
|
152
143
|
const slackFetchFile = createSlackFetchFile({ botToken: config.credentials?.botToken });
|
|
153
144
|
const stateAdapter = config.stateAdapter ?? createMemoryState();
|
|
154
|
-
const
|
|
145
|
+
const mergedEvents = {
|
|
146
|
+
...defaultEvents,
|
|
147
|
+
...config.events,
|
|
148
|
+
"input.requested": config.events?.["input.requested"] ?? defaultInputRequestedHandler(),
|
|
149
|
+
};
|
|
155
150
|
// The chat SDK defers mention handler invocation past the route
|
|
156
151
|
// handler returning, so the listener can't read `send` off the
|
|
157
152
|
// current request. Capture it on the first request and reuse it —
|
|
@@ -166,7 +161,7 @@ export function slackChannel(config = {}) {
|
|
|
166
161
|
chatPromise = (async () => {
|
|
167
162
|
const { botToken, signingSecret, webhookVerifier } = resolveSlackAdapterCredentials(config.credentials);
|
|
168
163
|
if (!botToken) {
|
|
169
|
-
throw new Error("slackChannel requires a bot token. Pass credentials.botToken or set SLACK_BOT_TOKEN.");
|
|
164
|
+
throw new Error("slackChannel() requires a bot token. Pass credentials.botToken or set SLACK_BOT_TOKEN.");
|
|
170
165
|
}
|
|
171
166
|
const [slackModule, chatModule] = await Promise.all([
|
|
172
167
|
import("#compiled/@chat-adapter/slack/index.js"),
|
|
@@ -226,7 +221,7 @@ export function slackChannel(config = {}) {
|
|
|
226
221
|
await getChat();
|
|
227
222
|
const channelId = input.args.channelId;
|
|
228
223
|
if (!channelId) {
|
|
229
|
-
throw new Error("slackChannel.receive requires args.channelId.");
|
|
224
|
+
throw new Error("slackChannel().receive requires args.channelId.");
|
|
230
225
|
}
|
|
231
226
|
const chatModule = await import("#compiled/chat/index.js");
|
|
232
227
|
const thread = new chatModule.ThreadImpl({
|
|
@@ -245,9 +240,6 @@ export function slackChannel(config = {}) {
|
|
|
245
240
|
},
|
|
246
241
|
});
|
|
247
242
|
},
|
|
248
|
-
events:
|
|
249
|
-
...config.events,
|
|
250
|
-
"input.requested": inputHandler,
|
|
251
|
-
},
|
|
243
|
+
events: mergedEvents,
|
|
252
244
|
});
|
|
253
245
|
}
|
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 };
|