experimental-ash 0.10.2 → 0.10.3
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 +7 -0
- package/dist/src/chunks/{dev-authored-source-watcher-CDT0dQQf.js → dev-authored-source-watcher-Dli96TWx.js} +1 -1
- package/dist/src/chunks/{host-M56X595D.js → host-Cp4XhAAX.js} +2 -2
- package/dist/src/chunks/{paths-B-Onq-sx.js → paths-CWTvKBbf.js} +1 -1
- package/dist/src/chunks/{prewarm-DQzlGOTi.js → prewarm-DIMA-oZ6.js} +1 -1
- package/dist/src/cli/commands/info.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +5 -5
- package/dist/src/compiled/@ai-sdk/anthropic/index.js +2 -2
- package/dist/src/compiled/@ai-sdk/anthropic/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/google/index.js +8 -3
- package/dist/src/compiled/@ai-sdk/google/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/mcp/index.js +1 -1
- package/dist/src/compiled/@ai-sdk/mcp/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/openai/index.js +10 -6
- package/dist/src/compiled/@ai-sdk/openai/package.json +1 -1
- package/dist/src/compiled/@ai-sdk/otel/index.js +3 -3
- package/dist/src/compiled/@ai-sdk/otel/package.json +1 -1
- package/dist/src/compiled/@workflow/core/index.js +1 -1
- package/dist/src/compiled/@workflow/core/runtime.js +5 -5
- package/dist/src/compiled/@workflow/core/workflow.js +1 -1
- package/dist/src/compiled/@workflow/errors/index.js +1 -1
- package/dist/src/compiled/_chunks/workflow/{context-errors-zbKocOyk.js → context-errors-CmtmBosi.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/dist-4zn5tehu.js +10 -0
- package/dist/src/compiled/_chunks/workflow/dist-DTWUhyDN.js +5 -0
- package/dist/src/compiled/_chunks/workflow/{resume-hook-CL8Ed91K.js → resume-hook-BqY8TqOE.js} +2 -2
- package/dist/src/compiled/_chunks/workflow/sleep-D30F1GSr.js +1 -0
- package/dist/src/evals/cli/eval.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/public/channels/slack/api.d.ts +34 -0
- package/dist/src/public/channels/slack/api.js +55 -0
- package/dist/src/public/channels/slack/attachments.d.ts +17 -1
- package/dist/src/public/channels/slack/attachments.js +41 -0
- package/dist/src/public/channels/slack/connections.d.ts +57 -0
- package/dist/src/public/channels/slack/connections.js +70 -0
- package/dist/src/public/channels/slack/hitl.d.ts +74 -0
- package/dist/src/public/channels/slack/hitl.js +136 -9
- package/dist/src/public/channels/slack/inbound.d.ts +55 -0
- package/dist/src/public/channels/slack/inbound.js +75 -0
- package/dist/src/public/channels/slack/index.d.ts +1 -1
- package/dist/src/public/channels/slack/interactions.d.ts +74 -0
- package/dist/src/public/channels/slack/interactions.js +311 -0
- package/dist/src/public/channels/slack/limits.d.ts +43 -0
- package/dist/src/public/channels/slack/limits.js +52 -0
- package/dist/src/public/channels/slack/slack.d.ts +12 -2
- package/dist/src/public/channels/slack/slack.js +168 -12
- package/dist/src/public/channels/slack/slackChannel.d.ts +47 -1
- package/dist/src/public/channels/slack/slackChannel.js +41 -138
- package/dist/src/public/definitions/defineChannel.d.ts +2 -0
- package/dist/src/public/definitions/defineChannel.js +2 -0
- package/dist/src/shared/sandbox-session.d.ts +1 -1
- package/package.json +8 -8
- package/dist/src/compiled/_chunks/workflow/dist-Ci2brnHh.js +0 -14
- package/dist/src/compiled/_chunks/workflow/sleep-Dn3i9nxI.js +0 -1
- /package/dist/src/compiled/_chunks/workflow/{dist-0iNBqPYp.js → dist-B6aByiku.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{dist-D774SUM4.js → dist-CVo7knbW.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{src-ClRYdO4-.js → src-Bc9OYRaN.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{symbols-D-4tVV8x.js → symbols-DkV1V0kM.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{token-CsNmv7KW.js → token-Cq5QjRq8.js} +0 -0
- /package/dist/src/compiled/_chunks/workflow/{token-j5Cl4rrs.js → token-Duaoxfi5.js} +0 -0
|
@@ -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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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 };
|