experimental-ash 0.10.2 → 0.10.4

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/src/chunks/{dev-authored-source-watcher-CDT0dQQf.js → dev-authored-source-watcher-Dli96TWx.js} +1 -1
  3. package/dist/src/chunks/{host-M56X595D.js → host-Cp4XhAAX.js} +2 -2
  4. package/dist/src/chunks/{paths-B-Onq-sx.js → paths-CWTvKBbf.js} +1 -1
  5. package/dist/src/chunks/{prewarm-DQzlGOTi.js → prewarm-DIMA-oZ6.js} +1 -1
  6. package/dist/src/cli/commands/info.js +1 -1
  7. package/dist/src/cli/run.js +1 -1
  8. package/dist/src/compiled/.vendor-stamp.json +5 -5
  9. package/dist/src/compiled/@ai-sdk/anthropic/index.js +2 -2
  10. package/dist/src/compiled/@ai-sdk/anthropic/package.json +1 -1
  11. package/dist/src/compiled/@ai-sdk/google/index.js +8 -3
  12. package/dist/src/compiled/@ai-sdk/google/package.json +1 -1
  13. package/dist/src/compiled/@ai-sdk/mcp/index.js +1 -1
  14. package/dist/src/compiled/@ai-sdk/mcp/package.json +1 -1
  15. package/dist/src/compiled/@ai-sdk/openai/index.js +10 -6
  16. package/dist/src/compiled/@ai-sdk/openai/package.json +1 -1
  17. package/dist/src/compiled/@ai-sdk/otel/index.js +3 -3
  18. package/dist/src/compiled/@ai-sdk/otel/package.json +1 -1
  19. package/dist/src/compiled/@workflow/core/index.js +1 -1
  20. package/dist/src/compiled/@workflow/core/runtime.js +5 -5
  21. package/dist/src/compiled/@workflow/core/workflow.js +1 -1
  22. package/dist/src/compiled/@workflow/errors/index.js +1 -1
  23. package/dist/src/compiled/_chunks/workflow/{context-errors-zbKocOyk.js → context-errors-CmtmBosi.js} +1 -1
  24. package/dist/src/compiled/_chunks/workflow/dist-4zn5tehu.js +10 -0
  25. package/dist/src/compiled/_chunks/workflow/dist-DTWUhyDN.js +5 -0
  26. package/dist/src/compiled/_chunks/workflow/{resume-hook-CL8Ed91K.js → resume-hook-BqY8TqOE.js} +2 -2
  27. package/dist/src/compiled/_chunks/workflow/sleep-D30F1GSr.js +1 -0
  28. package/dist/src/evals/cli/eval.js +1 -1
  29. package/dist/src/harness/input-requests.js +46 -18
  30. package/dist/src/internal/application/package.js +1 -1
  31. package/dist/src/public/channels/slack/api.d.ts +34 -0
  32. package/dist/src/public/channels/slack/api.js +55 -0
  33. package/dist/src/public/channels/slack/attachments.d.ts +17 -1
  34. package/dist/src/public/channels/slack/attachments.js +41 -0
  35. package/dist/src/public/channels/slack/connections.d.ts +57 -0
  36. package/dist/src/public/channels/slack/connections.js +70 -0
  37. package/dist/src/public/channels/slack/hitl.d.ts +74 -0
  38. package/dist/src/public/channels/slack/hitl.js +136 -9
  39. package/dist/src/public/channels/slack/inbound.d.ts +55 -0
  40. package/dist/src/public/channels/slack/inbound.js +75 -0
  41. package/dist/src/public/channels/slack/index.d.ts +1 -1
  42. package/dist/src/public/channels/slack/interactions.d.ts +74 -0
  43. package/dist/src/public/channels/slack/interactions.js +311 -0
  44. package/dist/src/public/channels/slack/limits.d.ts +43 -0
  45. package/dist/src/public/channels/slack/limits.js +52 -0
  46. package/dist/src/public/channels/slack/slack.d.ts +12 -2
  47. package/dist/src/public/channels/slack/slack.js +168 -12
  48. package/dist/src/public/channels/slack/slackChannel.d.ts +47 -1
  49. package/dist/src/public/channels/slack/slackChannel.js +41 -138
  50. package/dist/src/public/definitions/defineChannel.d.ts +2 -0
  51. package/dist/src/public/definitions/defineChannel.js +2 -0
  52. package/dist/src/shared/sandbox-session.d.ts +1 -1
  53. package/package.json +8 -8
  54. package/dist/src/compiled/_chunks/workflow/dist-Ci2brnHh.js +0 -14
  55. package/dist/src/compiled/_chunks/workflow/sleep-Dn3i9nxI.js +0 -1
  56. /package/dist/src/compiled/_chunks/workflow/{dist-0iNBqPYp.js → dist-B6aByiku.js} +0 -0
  57. /package/dist/src/compiled/_chunks/workflow/{dist-D774SUM4.js → dist-CVo7knbW.js} +0 -0
  58. /package/dist/src/compiled/_chunks/workflow/{src-ClRYdO4-.js → src-Bc9OYRaN.js} +0 -0
  59. /package/dist/src/compiled/_chunks/workflow/{symbols-D-4tVV8x.js → symbols-DkV1V0kM.js} +0 -0
  60. /package/dist/src/compiled/_chunks/workflow/{token-CsNmv7KW.js → token-Cq5QjRq8.js} +0 -0
  61. /package/dist/src/compiled/_chunks/workflow/{token-j5Cl4rrs.js → token-Duaoxfi5.js} +0 -0
@@ -44,6 +44,47 @@ function toSlackFilePart(attachment, index) {
44
44
  data: new URL(attachment.url),
45
45
  };
46
46
  }
47
+ /**
48
+ * Collects file parts for an inbound mention.
49
+ *
50
+ * Prefers attachments on the triggering mention (the common case: user
51
+ * uploads a file and mentions the bot in the same message). When the
52
+ * mention has none, refreshes the thread and picks the latest non-bot
53
+ * message's attachments — covering the case where a user dropped a file
54
+ * in the thread first, then mentioned the bot in a follow-up. Any error
55
+ * during refresh is logged and treated as "no attachments" so the text
56
+ * portion of the mention still gets delivered.
57
+ */
58
+ export async function collectInboundFileParts(input) {
59
+ const fromMention = collectSlackFileParts(input.mention, input.policy);
60
+ if (fromMention.length > 0)
61
+ return fromMention;
62
+ try {
63
+ if (typeof input.thread.refresh === "function") {
64
+ await input.thread.refresh();
65
+ }
66
+ }
67
+ catch (error) {
68
+ log.warn("slack thread refresh failed for attachment collection", { error });
69
+ return [];
70
+ }
71
+ const recent = input.thread.recentMessages ?? [];
72
+ for (let i = recent.length - 1; i >= 0; i -= 1) {
73
+ const candidate = recent[i];
74
+ if (!candidate)
75
+ continue;
76
+ const author = candidate.author;
77
+ if (author?.isMe)
78
+ continue;
79
+ const parts = collectSlackFileParts(candidate, input.policy);
80
+ if (parts.length > 0)
81
+ return parts;
82
+ // First non-bot message had no attachments — no need to scan further;
83
+ // pre-474 behavior was "latest non-bot message wins" not "any in thread".
84
+ return [];
85
+ }
86
+ return [];
87
+ }
47
88
  /**
48
89
  * Combines text + file parts into the {@link UserContent} shape the
49
90
  * harness expects. Returns the raw text string when there are no
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Slack rendering for `connection.authorization_*` events.
3
+ *
4
+ * The framework emits these when a tool call needs the user to complete
5
+ * an OAuth-style authorization flow (e.g. signing in to Linear). The
6
+ * channel's default handler posts:
7
+ *
8
+ * 1. An ephemeral "Sign in with X" link button to the triggering user
9
+ * (when their id is known), surfacing the challenge URL.
10
+ * 2. A public "Connect with X to continue" status message visible to
11
+ * everyone in the thread.
12
+ *
13
+ * When the matching `connection.authorization_completed` event arrives
14
+ * the public status message is edited in place to surface the outcome
15
+ * (`authorized` / `declined` / `failed` / `timed-out`).
16
+ */
17
+ /**
18
+ * Outcomes carried on a `connection.authorization_completed` event.
19
+ * Mirrors the framework event shape so the renderer needs no extra
20
+ * imports.
21
+ */
22
+ export type ConnectionAuthorizationOutcome = "authorized" | "declined" | "failed" | "timed-out";
23
+ /**
24
+ * Title-cases a connection name (`linear` → `Linear`) for display. Empty
25
+ * strings pass through unchanged so the renderer never emits an empty
26
+ * label inside a sentence.
27
+ */
28
+ export declare function formatConnectionDisplayName(connectionName: string): string;
29
+ /**
30
+ * Public status text posted when an authorization challenge fires. When
31
+ * the channel cannot identify a triggering user (rare — schedule-initiated
32
+ * sessions or events that lack actor metadata) the text drops the
33
+ * "Connect with" call-to-action since there's no one to act on it.
34
+ */
35
+ export declare function buildAuthRequiredPublicText(input: {
36
+ readonly displayName: string;
37
+ readonly hasUser: boolean;
38
+ }): string;
39
+ /**
40
+ * Final-state markdown for the previously-posted status message. Edited
41
+ * in place by `connection.authorization_completed` so the user sees
42
+ * resolution without scrolling.
43
+ */
44
+ export declare function buildAuthCompletedText(input: {
45
+ readonly displayName: string;
46
+ readonly outcome: ConnectionAuthorizationOutcome;
47
+ readonly reason?: string;
48
+ }): string;
49
+ /**
50
+ * Block Kit blocks for the ephemeral "Sign in with X" link button.
51
+ * Slack ephemerals accept the same block list shape as regular messages
52
+ * so the helper returns blocks directly.
53
+ */
54
+ export declare function buildAuthEphemeralBlocks(input: {
55
+ readonly displayName: string;
56
+ readonly url: string;
57
+ }): unknown[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Slack rendering for `connection.authorization_*` events.
3
+ *
4
+ * The framework emits these when a tool call needs the user to complete
5
+ * an OAuth-style authorization flow (e.g. signing in to Linear). The
6
+ * channel's default handler posts:
7
+ *
8
+ * 1. An ephemeral "Sign in with X" link button to the triggering user
9
+ * (when their id is known), surfacing the challenge URL.
10
+ * 2. A public "Connect with X to continue" status message visible to
11
+ * everyone in the thread.
12
+ *
13
+ * When the matching `connection.authorization_completed` event arrives
14
+ * the public status message is edited in place to surface the outcome
15
+ * (`authorized` / `declined` / `failed` / `timed-out`).
16
+ */
17
+ /**
18
+ * Title-cases a connection name (`linear` → `Linear`) for display. Empty
19
+ * strings pass through unchanged so the renderer never emits an empty
20
+ * label inside a sentence.
21
+ */
22
+ export function formatConnectionDisplayName(connectionName) {
23
+ if (connectionName.length === 0)
24
+ return connectionName;
25
+ return connectionName.charAt(0).toUpperCase() + connectionName.slice(1);
26
+ }
27
+ /**
28
+ * Public status text posted when an authorization challenge fires. When
29
+ * the channel cannot identify a triggering user (rare — schedule-initiated
30
+ * sessions or events that lack actor metadata) the text drops the
31
+ * "Connect with" call-to-action since there's no one to act on it.
32
+ */
33
+ export function buildAuthRequiredPublicText(input) {
34
+ if (!input.hasUser) {
35
+ return `Authorization required for ${input.displayName} (no triggering user)`;
36
+ }
37
+ return `Connect with ${input.displayName} to continue`;
38
+ }
39
+ /**
40
+ * Final-state markdown for the previously-posted status message. Edited
41
+ * in place by `connection.authorization_completed` so the user sees
42
+ * resolution without scrolling.
43
+ */
44
+ export function buildAuthCompletedText(input) {
45
+ if (input.outcome === "authorized") {
46
+ return `:white_check_mark: ${input.displayName} connected`;
47
+ }
48
+ const tail = input.reason !== undefined ? ` (${input.reason})` : "";
49
+ return `:x: ${input.displayName} authorization ${input.outcome}${tail}`;
50
+ }
51
+ /**
52
+ * Block Kit blocks for the ephemeral "Sign in with X" link button.
53
+ * Slack ephemerals accept the same block list shape as regular messages
54
+ * so the helper returns blocks directly.
55
+ */
56
+ export function buildAuthEphemeralBlocks(input) {
57
+ return [
58
+ {
59
+ type: "actions",
60
+ elements: [
61
+ {
62
+ type: "button",
63
+ text: { type: "plain_text", text: `Sign in with ${input.displayName}` },
64
+ url: input.url,
65
+ style: "primary",
66
+ },
67
+ ],
68
+ },
69
+ ];
70
+ }
@@ -19,6 +19,28 @@ import type { InputRequest } from "#runtime/input/types.js";
19
19
  * interactive widgets can avoid collisions.
20
20
  */
21
21
  export declare const HITL_ACTION_PREFIX = "ash_input:";
22
+ /**
23
+ * `action_id` prefix for the "Type your answer" button that opens a
24
+ * freeform-answer modal. Splitting the prefix from {@link HITL_ACTION_PREFIX}
25
+ * lets the route handler differentiate "this click is a final answer"
26
+ * (resolve via `inputResponses`) from "this click needs a modal first"
27
+ * (call `views.open`, then resolve on `view_submission`).
28
+ */
29
+ export declare const HITL_FREEFORM_ACTION_PREFIX = "ash_input_freeform:";
30
+ /**
31
+ * `view.callback_id` carried on the freeform-answer modal. Used to
32
+ * route the inbound `view_submission` back to this channel.
33
+ */
34
+ export declare const HITL_FREEFORM_MODAL_CALLBACK_ID = "ash_input_freeform_submit";
35
+ /**
36
+ * `block_id` of the modal's text-input block — the route reads the
37
+ * submitted text out of `view.state.values[block_id][action_id]`.
38
+ */
39
+ export declare const HITL_FREEFORM_MODAL_BLOCK_ID = "ash_freeform_block";
40
+ /**
41
+ * `action_id` of the text input inside the freeform answer modal.
42
+ */
43
+ export declare const HITL_FREEFORM_MODAL_ACTION_ID = "ash_freeform_text";
22
44
  /**
23
45
  * Subset of one Slack interactivity action the HITL decoder reads.
24
46
  * Mirrors the relevant fields of `SlackInteractionAction`.
@@ -61,7 +83,59 @@ export declare function isHitlAction(actionId: string): boolean;
61
83
  * dropdown so the picker stays scrollable.
62
84
  * - Anything else with options → buttons. Best for visually distinct
63
85
  * choices (approve / deny / cancel).
86
+ * - No options (or `allowFreeform: true`) → a single "Type your answer"
87
+ * button that opens a Slack modal with a plain_text_input. The modal
88
+ * submission comes back as a `view_submission` webhook the channel
89
+ * resolves into an {@link InputResponse} carrying `text`.
64
90
  *
65
91
  * Always emits at least the prompt section.
66
92
  */
67
93
  export declare function renderInputRequestBlocks(request: InputRequest): unknown[];
94
+ /**
95
+ * Metadata round-tripped on the freeform-answer modal's
96
+ * `private_metadata` field. Threaded from the button click that opens
97
+ * the modal to the `view_submission` that closes it so the route can
98
+ * deliver the answer back to the right session.
99
+ */
100
+ export interface HitlFreeformModalMetadata {
101
+ readonly continuationToken: string;
102
+ readonly channelId: string;
103
+ readonly threadTs: string;
104
+ readonly messageTs: string;
105
+ readonly requestId: string;
106
+ }
107
+ /**
108
+ * Builds the `views.open` payload for the freeform-answer modal. The
109
+ * triggering `prompt` is preserved as a header section so the user can
110
+ * re-read what they're answering inside the modal.
111
+ *
112
+ * Title is auto-truncated to the Slack modal-title limit.
113
+ */
114
+ export declare function buildFreeformModalView(input: {
115
+ readonly metadata: HitlFreeformModalMetadata;
116
+ readonly prompt?: string;
117
+ }): Record<string, unknown>;
118
+ /**
119
+ * True when an `action_id` was minted by the framework's freeform-answer
120
+ * button (the click that opens a modal — not the final answer).
121
+ */
122
+ export declare function isFreeformAction(actionId: string): boolean;
123
+ /**
124
+ * Extracts the requestId from a freeform-answer button's `action_id`.
125
+ */
126
+ export declare function freeformRequestIdFromActionId(actionId: string): string | undefined;
127
+ /**
128
+ * Renders the "answered" replacement blocks for a previously-posted
129
+ * HITL card. Preserves the original prompt block (so context stays
130
+ * visible), appends a confirmation line naming the chosen answer, and
131
+ * attributes the click to the user when their id is known.
132
+ *
133
+ * Slack's `chat.update` replaces every block in one shot, so the caller
134
+ * passes the full list to `blocks` and the rendered fallback text to
135
+ * `text`.
136
+ */
137
+ export declare function buildAnsweredBlocks(input: {
138
+ readonly promptBlock: unknown;
139
+ readonly answerLabel: string;
140
+ readonly userId?: string;
141
+ }): unknown[];
@@ -12,12 +12,35 @@
12
12
  * picks whichever is set so the renderer can pick a widget kind on
13
13
  * UX grounds without changing the read path.
14
14
  */
15
+ import { truncateModalTitle, truncatePlainText } from "#public/channels/slack/limits.js";
15
16
  /**
16
17
  * Wire-format prefix every framework HITL widget mints onto its
17
18
  * `action_id`. Exposed so end-user adapters that render their own
18
19
  * interactive widgets can avoid collisions.
19
20
  */
20
21
  export const HITL_ACTION_PREFIX = "ash_input:";
22
+ /**
23
+ * `action_id` prefix for the "Type your answer" button that opens a
24
+ * freeform-answer modal. Splitting the prefix from {@link HITL_ACTION_PREFIX}
25
+ * lets the route handler differentiate "this click is a final answer"
26
+ * (resolve via `inputResponses`) from "this click needs a modal first"
27
+ * (call `views.open`, then resolve on `view_submission`).
28
+ */
29
+ export const HITL_FREEFORM_ACTION_PREFIX = "ash_input_freeform:";
30
+ /**
31
+ * `view.callback_id` carried on the freeform-answer modal. Used to
32
+ * route the inbound `view_submission` back to this channel.
33
+ */
34
+ export const HITL_FREEFORM_MODAL_CALLBACK_ID = "ash_input_freeform_submit";
35
+ /**
36
+ * `block_id` of the modal's text-input block — the route reads the
37
+ * submitted text out of `view.state.values[block_id][action_id]`.
38
+ */
39
+ export const HITL_FREEFORM_MODAL_BLOCK_ID = "ash_freeform_block";
40
+ /**
41
+ * `action_id` of the text input inside the freeform answer modal.
42
+ */
43
+ export const HITL_FREEFORM_MODAL_ACTION_ID = "ash_freeform_text";
21
44
  /**
22
45
  * Maximum radio-button option count before the renderer falls back to
23
46
  * a `static_select` dropdown. Matches Slack's UX guidance (radio
@@ -54,6 +77,10 @@ export function isHitlAction(actionId) {
54
77
  * dropdown so the picker stays scrollable.
55
78
  * - Anything else with options → buttons. Best for visually distinct
56
79
  * choices (approve / deny / cancel).
80
+ * - No options (or `allowFreeform: true`) → a single "Type your answer"
81
+ * button that opens a Slack modal with a plain_text_input. The modal
82
+ * submission comes back as a `view_submission` webhook the channel
83
+ * resolves into an {@link InputResponse} carrying `text`.
57
84
  *
58
85
  * Always emits at least the prompt section.
59
86
  */
@@ -61,10 +88,8 @@ export function renderInputRequestBlocks(request) {
61
88
  const prompt = { text: { text: request.prompt, type: "mrkdwn" }, type: "section" };
62
89
  const actionId = `${HITL_ACTION_PREFIX}${request.requestId}`;
63
90
  const options = request.options;
64
- if (!options || options.length === 0) {
65
- return [prompt];
66
- }
67
- if (request.display === "select") {
91
+ const acceptsFreeform = request.allowFreeform === true || !options || options.length === 0;
92
+ if (options && options.length > 0 && request.display === "select") {
68
93
  const widget = options.length <= RADIO_SELECT_OPTION_LIMIT
69
94
  ? { type: "radio_buttons", action_id: actionId, options: options.map(buildOption) }
70
95
  : {
@@ -75,12 +100,86 @@ export function renderInputRequestBlocks(request) {
75
100
  };
76
101
  return [prompt, { type: "actions", elements: [widget] }];
77
102
  }
78
- return [prompt, { type: "actions", elements: options.map((opt) => buildButton(opt, actionId)) }];
103
+ if (options && options.length > 0) {
104
+ return [
105
+ prompt,
106
+ { type: "actions", elements: options.map((opt) => buildButton(opt, actionId)) },
107
+ ];
108
+ }
109
+ if (acceptsFreeform) {
110
+ return [
111
+ prompt,
112
+ {
113
+ type: "actions",
114
+ elements: [
115
+ {
116
+ type: "button",
117
+ action_id: `${HITL_FREEFORM_ACTION_PREFIX}${request.requestId}`,
118
+ text: { type: "plain_text", text: "Type your answer" },
119
+ style: "primary",
120
+ value: request.requestId,
121
+ },
122
+ ],
123
+ },
124
+ ];
125
+ }
126
+ return [prompt];
127
+ }
128
+ /**
129
+ * Builds the `views.open` payload for the freeform-answer modal. The
130
+ * triggering `prompt` is preserved as a header section so the user can
131
+ * re-read what they're answering inside the modal.
132
+ *
133
+ * Title is auto-truncated to the Slack modal-title limit.
134
+ */
135
+ export function buildFreeformModalView(input) {
136
+ const title = input.prompt ? truncateModalTitle(input.prompt) : "Your answer";
137
+ const promptBlocks = input.prompt
138
+ ? [{ type: "section", text: { type: "mrkdwn", text: input.prompt } }]
139
+ : [];
140
+ return {
141
+ type: "modal",
142
+ callback_id: HITL_FREEFORM_MODAL_CALLBACK_ID,
143
+ private_metadata: JSON.stringify(input.metadata),
144
+ title: { type: "plain_text", text: title },
145
+ submit: { type: "plain_text", text: "Submit" },
146
+ close: { type: "plain_text", text: "Cancel" },
147
+ blocks: [
148
+ ...promptBlocks,
149
+ {
150
+ type: "input",
151
+ block_id: HITL_FREEFORM_MODAL_BLOCK_ID,
152
+ element: {
153
+ type: "plain_text_input",
154
+ action_id: HITL_FREEFORM_MODAL_ACTION_ID,
155
+ multiline: true,
156
+ placeholder: { type: "plain_text", text: "Type your answer here..." },
157
+ },
158
+ label: { type: "plain_text", text: "Answer" },
159
+ },
160
+ ],
161
+ };
162
+ }
163
+ /**
164
+ * True when an `action_id` was minted by the framework's freeform-answer
165
+ * button (the click that opens a modal — not the final answer).
166
+ */
167
+ export function isFreeformAction(actionId) {
168
+ return actionId.startsWith(HITL_FREEFORM_ACTION_PREFIX);
169
+ }
170
+ /**
171
+ * Extracts the requestId from a freeform-answer button's `action_id`.
172
+ */
173
+ export function freeformRequestIdFromActionId(actionId) {
174
+ if (!isFreeformAction(actionId))
175
+ return undefined;
176
+ const slice = actionId.slice(HITL_FREEFORM_ACTION_PREFIX.length);
177
+ return slice.length > 0 ? slice : undefined;
79
178
  }
80
179
  function buildButton(opt, actionId) {
81
180
  const button = {
82
181
  action_id: actionId,
83
- text: { text: opt.label, type: "plain_text" },
182
+ text: { text: truncatePlainText(opt.label), type: "plain_text" },
84
183
  type: "button",
85
184
  value: opt.id,
86
185
  };
@@ -91,11 +190,39 @@ function buildButton(opt, actionId) {
91
190
  }
92
191
  function buildOption(opt) {
93
192
  const option = {
94
- text: { text: opt.label, type: "plain_text" },
193
+ text: { text: truncatePlainText(opt.label), type: "plain_text" },
95
194
  value: opt.id,
96
195
  };
97
- if (opt.description && opt.description.length > 0) {
98
- option.description = { text: opt.description, type: "plain_text" };
196
+ const description = truncatePlainText(opt.description);
197
+ if (description && description.length > 0) {
198
+ option.description = { text: description, type: "plain_text" };
99
199
  }
100
200
  return option;
101
201
  }
202
+ /**
203
+ * Renders the "answered" replacement blocks for a previously-posted
204
+ * HITL card. Preserves the original prompt block (so context stays
205
+ * visible), appends a confirmation line naming the chosen answer, and
206
+ * attributes the click to the user when their id is known.
207
+ *
208
+ * Slack's `chat.update` replaces every block in one shot, so the caller
209
+ * passes the full list to `blocks` and the rendered fallback text to
210
+ * `text`.
211
+ */
212
+ export function buildAnsweredBlocks(input) {
213
+ const blocks = [];
214
+ if (input.promptBlock !== undefined && input.promptBlock !== null) {
215
+ blocks.push(input.promptBlock);
216
+ }
217
+ blocks.push({
218
+ type: "section",
219
+ text: { type: "mrkdwn", text: `:white_check_mark: *${input.answerLabel}*` },
220
+ });
221
+ if (input.userId && input.userId.length > 0) {
222
+ blocks.push({
223
+ type: "context",
224
+ elements: [{ type: "mrkdwn", text: `Answered by <@${input.userId}>` }],
225
+ });
226
+ }
227
+ return blocks;
228
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Inbound mention-text shaping.
3
+ *
4
+ * The channel calls these helpers on every inbound `app_mention` before
5
+ * handing the text to {@link buildSlackTurnMessage}:
6
+ *
7
+ * 1. {@link renderInboundText} converts the chat SDK's formatted AST
8
+ * back to GFM so the agent sees `[label](url)` / `**bold**` /
9
+ * bullets / mentions instead of the raw `<@U…>` / `<https://…|…>`
10
+ * fragments that `message.text` strips formatting down to.
11
+ * 2. {@link prependSlackContext} attaches a `<slack_context>` block
12
+ * naming the actor, channel, and thread so the agent's prompt always
13
+ * knows who and where it is talking.
14
+ */
15
+ import { type Message } from "#compiled/chat/index.js";
16
+ import type { UserContent } from "ai";
17
+ /**
18
+ * Verified inbound identity used to render a `<slack_context>` block.
19
+ *
20
+ * Channel-owned shape so the helper does not depend on the chat SDK
21
+ * `Message` type (and is therefore trivially testable in isolation).
22
+ */
23
+ export interface SlackInboundContext {
24
+ readonly userId: string;
25
+ readonly userName?: string;
26
+ readonly fullName?: string;
27
+ readonly channelId: string;
28
+ readonly threadTs: string;
29
+ readonly teamId?: string;
30
+ }
31
+ /**
32
+ * Re-renders a chat SDK message back to GFM markdown.
33
+ *
34
+ * The chat SDK parses Slack's inbound mrkdwn into a structured AST on
35
+ * `message.formatted`. Stringifying that AST preserves hyperlinks,
36
+ * mentions, and inline emphasis that `message.text` strips. Falls back
37
+ * to `message.text` when the AST is missing or fails to stringify so a
38
+ * misbehaving formatter never blocks an inbound turn.
39
+ */
40
+ export declare function renderInboundText(message: Message): string;
41
+ /**
42
+ * Renders one {@link SlackInboundContext} as a `<slack_context>` block.
43
+ * Lines are deterministic and tag-delimited so the agent can pattern-match
44
+ * the block out of its prompt if it wants to.
45
+ */
46
+ export declare function formatSlackContextBlock(context: SlackInboundContext): string;
47
+ /**
48
+ * Prepends a `<slack_context>` block to the inbound turn message.
49
+ *
50
+ * Accepts either a raw string (no attachments) or a {@link UserContent}
51
+ * array (text + file parts). In the array case the context block lands
52
+ * as the first {@link TextPart} — followed by the original parts — so
53
+ * attachments stay intact.
54
+ */
55
+ export declare function prependSlackContext(message: string | UserContent, context: SlackInboundContext): string | UserContent;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Inbound mention-text shaping.
3
+ *
4
+ * The channel calls these helpers on every inbound `app_mention` before
5
+ * handing the text to {@link buildSlackTurnMessage}:
6
+ *
7
+ * 1. {@link renderInboundText} converts the chat SDK's formatted AST
8
+ * back to GFM so the agent sees `[label](url)` / `**bold**` /
9
+ * bullets / mentions instead of the raw `<@U…>` / `<https://…|…>`
10
+ * fragments that `message.text` strips formatting down to.
11
+ * 2. {@link prependSlackContext} attaches a `<slack_context>` block
12
+ * naming the actor, channel, and thread so the agent's prompt always
13
+ * knows who and where it is talking.
14
+ */
15
+ import { stringifyMarkdown } from "#compiled/chat/index.js";
16
+ import { createLogger } from "#internal/logging.js";
17
+ const log = createLogger("slack.inbound");
18
+ /**
19
+ * Re-renders a chat SDK message back to GFM markdown.
20
+ *
21
+ * The chat SDK parses Slack's inbound mrkdwn into a structured AST on
22
+ * `message.formatted`. Stringifying that AST preserves hyperlinks,
23
+ * mentions, and inline emphasis that `message.text` strips. Falls back
24
+ * to `message.text` when the AST is missing or fails to stringify so a
25
+ * misbehaving formatter never blocks an inbound turn.
26
+ */
27
+ export function renderInboundText(message) {
28
+ const fallback = typeof message.text === "string" ? message.text : "";
29
+ const formatted = message.formatted;
30
+ if (formatted === null || formatted === undefined) {
31
+ return fallback;
32
+ }
33
+ try {
34
+ const rendered = stringifyMarkdown(formatted).trim();
35
+ return rendered.length > 0 ? rendered : fallback;
36
+ }
37
+ catch (error) {
38
+ log.warn("stringifyMarkdown failed — falling back to plain text", { error });
39
+ return fallback;
40
+ }
41
+ }
42
+ /**
43
+ * Renders one {@link SlackInboundContext} as a `<slack_context>` block.
44
+ * Lines are deterministic and tag-delimited so the agent can pattern-match
45
+ * the block out of its prompt if it wants to.
46
+ */
47
+ export function formatSlackContextBlock(context) {
48
+ const lines = [
49
+ "<slack_context>",
50
+ `user_id: ${context.userId}`,
51
+ ...(context.userName ? [`user_name: ${context.userName}`] : []),
52
+ ...(context.fullName ? [`full_name: ${context.fullName}`] : []),
53
+ `channel_id: ${context.channelId}`,
54
+ `thread_ts: ${context.threadTs}`,
55
+ ...(context.teamId ? [`team_id: ${context.teamId}`] : []),
56
+ "</slack_context>",
57
+ ];
58
+ return lines.join("\n");
59
+ }
60
+ /**
61
+ * Prepends a `<slack_context>` block to the inbound turn message.
62
+ *
63
+ * Accepts either a raw string (no attachments) or a {@link UserContent}
64
+ * array (text + file parts). In the array case the context block lands
65
+ * as the first {@link TextPart} — followed by the original parts — so
66
+ * attachments stay intact.
67
+ */
68
+ export function prependSlackContext(message, context) {
69
+ const block = formatSlackContextBlock(context);
70
+ if (typeof message === "string") {
71
+ return message.length > 0 ? `${block}\n\n${message}` : block;
72
+ }
73
+ const contextPart = { type: "text", text: block };
74
+ return [contextPart, ...message];
75
+ }
@@ -1,4 +1,4 @@
1
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 SlackInteractionAction, type SlackReceiveArgs, type SlackStateAdapter, type SlackWebhookVerifier, } from "#public/channels/slack/slackChannel.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";
3
3
  export { Actions, Button, Card, CardText, Divider, Fields, Image, LinkButton, Modal, RadioSelect, Section, Select, SelectOption, Table, TextInput, } from "#compiled/chat/index.js";
4
4
  export type { AdapterPostableMessage, Attachment, Author, CardElement, FileUpload, Message, PostableMessage, SentMessage, Thread, } from "#compiled/chat/index.js";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Slack `block_actions` + `view_submission` wire handling.
3
+ *
4
+ * The route handler reads the form-encoded body, hands it here, and we:
5
+ *
6
+ * 1. Decode `block_actions` payloads into a typed shape the channel can
7
+ * work with — actions, channel/thread metadata, the clicker, and the
8
+ * full original block list for answered-card updates.
9
+ * 2. Open the freeform-answer modal inline when the click was a "Type
10
+ * your answer" button (Slack's `trigger_id` is only valid for ~3s,
11
+ * so this can't run under `waitUntil`).
12
+ * 3. Resolve `view_submission` payloads (freeform modal submissions)
13
+ * back into parked HITL requests via `send`.
14
+ *
15
+ * Anything we don't consume flows through to the user-supplied
16
+ * `onInteraction` callback. Always returns `Response("ok")` — followup
17
+ * work runs under `waitUntil` so the webhook ACK is immediate.
18
+ */
19
+ import type { Message } from "#compiled/chat/index.js";
20
+ import type { SlackChannelConfig, SlackChannelState, SlackInteractionAction } from "#public/channels/slack/slackChannel.js";
21
+ import type { SendFn } from "#public/definitions/defineChannel.js";
22
+ /**
23
+ * Decoded view of a Slack `block_actions` payload. Returned by
24
+ * {@link parseBlockActionsPayload} and read by the handler.
25
+ */
26
+ interface ParsedBlockActionsPayload {
27
+ readonly actions: SlackInteractionAction[];
28
+ readonly channelId: string;
29
+ readonly threadTs: string;
30
+ readonly teamId: string | undefined;
31
+ /**
32
+ * Slack actor that authored the click. Used to attribute the answered
33
+ * card via `Answered by <@userId>` after a HITL response is delivered.
34
+ */
35
+ readonly userId: string | undefined;
36
+ /**
37
+ * The full block list off the clicked message. Preserved on the
38
+ * answered-card update so the original prompt stays visible after the
39
+ * interactive controls are stripped.
40
+ */
41
+ readonly messageBlocks: readonly unknown[];
42
+ }
43
+ /**
44
+ * Decodes a Slack `block_actions` payload into a {@link ParsedBlockActionsPayload}.
45
+ * Returns `null` for payloads that don't carry the channel/thread
46
+ * metadata the handler needs.
47
+ */
48
+ export declare function parseBlockActionsPayload(body: Record<string, unknown>): ParsedBlockActionsPayload | null;
49
+ /**
50
+ * Channel-supplied dependencies for {@link handleInteractionPost}.
51
+ *
52
+ * Carries the bits the handler needs that come from channel
53
+ * construction: credentials for outbound API calls, the user's
54
+ * `onInteraction` callback for non-HITL clicks, and a chat-SDK module
55
+ * supplier so the handler can build a thread for the user callback
56
+ * without statically depending on `#compiled/chat/index.js`.
57
+ */
58
+ export interface InteractionHandlerDeps {
59
+ readonly config: SlackChannelConfig;
60
+ readonly loadChatModule: () => Promise<typeof import("#compiled/chat/index.js")>;
61
+ }
62
+ /**
63
+ * Entry point for Slack's form-encoded interactivity endpoint. Routes
64
+ * `view_submission` payloads to the freeform-answer flow, intercepts
65
+ * "Type your answer" button clicks to open a modal, resolves
66
+ * framework HITL clicks against the parked session, and forwards
67
+ * anything else to `config.onInteraction`.
68
+ */
69
+ export declare function handleInteractionPost(rawBody: string, ctx: {
70
+ send: SendFn<SlackChannelState>;
71
+ waitUntil: (task: Promise<unknown>) => void;
72
+ }, deps: InteractionHandlerDeps): Promise<Response>;
73
+ export type { ParsedBlockActionsPayload };
74
+ export type { Message };