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.
@@ -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
- - `slack` - zero-config Slack channel
70
- - `slackChannel` - custom Slack channel with config object (`run`, event handlers, `onInteraction`)
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 { slack, slackChannel } from "experimental-ash/channels/slack";
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.10.4";
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 { slack, type SlackOptions } from "#public/channels/slack/slack.js";
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 `run`, `onMention`, and
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
- export type SlackRunResult = {
128
- auth: import("#channel/types.js").SessionAuthContext | null;
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 SlackRunResultOrPromise = SlackRunResult | Promise<SlackRunResult>;
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
- * Synchronous-or-async filter and auth resolver invoked the moment a
174
- * Slack `app_mention` arrives, before the framework dispatches a
175
- * turn. Return `{ auth }` to dispatch with that session auth context,
176
- * or `null` to silently drop the mention. Runs on the inbound webhook
177
- * side, so cold-start latency does not affect it.
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
- * The framework awaits this handler and catches thrown errors (the
188
- * turn still dispatches if `onMention` throws). It does not alter
189
- * dispatch to skip a turn, return `null` from `run()`.
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): void | Promise<void>;
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 { renderInputRequestBlocks } from "#public/channels/slack/hitl.js";
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.team_id ?? raw.team;
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
- const runOpts = config.run ? await config.run(slackCtx, message) : { auth: null };
104
- if (runOpts === null)
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: runOpts.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 inputHandler = config.events?.["input.requested"] ?? defaultInputRequestedHandler();
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,6 +1,6 @@
1
1
  {
2
2
  "name": "experimental-ash",
3
- "version": "0.10.4",
3
+ "version": "0.11.1",
4
4
  "bin": {
5
5
  "ash": "./bin/ash.js",
6
6
  "experimental-ash": "./bin/ash.js"
@@ -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 };