eve 0.84.1 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/src/channel/channel-operations.d.ts +2 -18
- package/dist/src/channel/compiled-channel.d.ts +3 -7
- package/dist/src/channel/cross-channel-receive.d.ts +9 -10
- package/dist/src/channel/cross-channel-receive.js +1 -1
- package/dist/src/channel/schedule.js +1 -1
- package/dist/src/channel/session.d.ts +26 -4
- package/dist/src/channel/session.js +1 -1
- package/dist/src/chunks/{use-eve-agent-DMeP0JOF.js → use-eve-agent-B2jCfcnG.js} +3 -2
- package/dist/src/chunks/{use-eve-agent-DP5Rso9C.js → use-eve-agent-DCLFi26P.js} +3 -2
- package/dist/src/client/client.js +1 -1
- package/dist/src/client/open-stream.js +1 -1
- package/dist/src/client/types.d.ts +5 -4
- package/dist/src/compiled/.vendor-stamp.json +1 -1
- package/dist/src/context/keys.d.ts +7 -0
- package/dist/src/context/keys.js +1 -1
- package/dist/src/execution/agent-sessions/context.d.ts +5 -1
- package/dist/src/execution/agent-sessions/context.js +1 -1
- package/dist/src/execution/dynamic-workflow/tool.js +1 -1
- package/dist/src/execution/legacy-session/snapshot.d.ts +1 -2
- package/dist/src/execution/legacy-session/snapshot.js +1 -1
- package/dist/src/execution/proxied-deliver-step.js +1 -1
- package/dist/src/execution/public-destination.d.ts +3 -0
- package/dist/src/execution/public-destination.js +1 -1
- package/dist/src/execution/publish-session-events.js +1 -1
- package/dist/src/execution/session/checkpoint-migrations.d.ts +16 -5
- package/dist/src/execution/session/checkpoint-migrations.js +1 -1
- package/dist/src/execution/session/checkpoint-refusal.d.ts +4 -0
- package/dist/src/execution/session/checkpoint-refusal.js +1 -0
- package/dist/src/execution/session/entry.js +1 -1
- package/dist/src/execution/session/handoff-steps.d.ts +7 -1
- package/dist/src/execution/session/handoff-steps.js +1 -1
- package/dist/src/execution/session/imported-conversation.d.ts +2 -0
- package/dist/src/execution/session/imported-conversation.js +1 -0
- package/dist/src/execution/session/interrupted-step.js +1 -1
- package/dist/src/execution/session/pre-v27-checkpoint.d.ts +36 -0
- package/dist/src/execution/session/pre-v27-checkpoint.js +1 -0
- package/dist/src/execution/session/program.d.ts +2 -0
- package/dist/src/execution/session/program.js +1 -1
- package/dist/src/execution/session/turn-step.js +1 -1
- package/dist/src/execution/session-callback-request.js +1 -1
- package/dist/src/execution/tool-auth.js +1 -1
- package/dist/src/execution/tools/agent-router-workflow.js +1 -1
- package/dist/src/execution/tools/channel-to.d.ts +12 -0
- package/dist/src/execution/tools/channel-to.js +1 -0
- package/dist/src/execution/tools/workflow/body.d.ts +3 -0
- package/dist/src/execution/tools/workflow/body.js +1 -1
- package/dist/src/execution/tools/workflow/step-context.d.ts +5 -0
- package/dist/src/execution/tools/workflow/step-execution.js +1 -1
- package/dist/src/execution/tools/workflow/step.js +1 -1
- package/dist/src/execution/workflow-callback-url.d.ts +2 -3
- package/dist/src/execution/workflow-callback-url.js +1 -1
- package/dist/src/execution/workflow-runtime.js +1 -1
- package/dist/src/harness/compaction/step.js +1 -1
- package/dist/src/harness/model-call/call.js +1 -1
- package/dist/src/harness/model-call/run.js +1 -1
- package/dist/src/harness/session-machine/transitions.js +1 -1
- package/dist/src/internal/application/stamped-package-version.js +1 -1
- package/dist/src/internal/attachments/limited-read.d.ts +2 -0
- package/dist/src/internal/attachments/limited-read.js +1 -1
- package/dist/src/internal/attachments/url-refs.js +1 -1
- package/dist/src/internal/capability-error.d.ts +3 -0
- package/dist/src/internal/capability-error.js +1 -0
- package/dist/src/public/channels/chat-sdk/chatSdkChannel.js +1 -1
- package/dist/src/public/channels/discord/discordChannel.js +1 -1
- package/dist/src/public/channels/github/githubChannel.js +1 -1
- package/dist/src/public/channels/linear/linearChannel.js +1 -1
- package/dist/src/public/channels/linq/inboundContent.js +1 -1
- package/dist/src/public/channels/slack/answers.d.ts +2 -3
- package/dist/src/public/channels/slack/answers.js +1 -1
- package/dist/src/public/channels/slack/api.d.ts +23 -3
- package/dist/src/public/channels/slack/api.js +1 -1
- package/dist/src/public/channels/slack/app-manifest.d.ts +2 -0
- package/dist/src/public/channels/slack/app-manifest.js +1 -1
- package/dist/src/public/channels/slack/auth.d.ts +2 -1
- package/dist/src/public/channels/slack/auth.js +1 -1
- package/dist/src/public/channels/slack/code-channel.d.ts +42 -0
- package/dist/src/public/channels/slack/code-channel.js +1 -0
- package/dist/src/public/channels/slack/defaults.d.ts +7 -3
- package/dist/src/public/channels/slack/defaults.js +3 -3
- package/dist/src/public/channels/slack/hitl.d.ts +7 -17
- package/dist/src/public/channels/slack/hitl.js +1 -1
- package/dist/src/public/channels/slack/index.d.ts +4 -3
- package/dist/src/public/channels/slack/index.js +1 -1
- package/dist/src/public/channels/slack/interaction-cards.d.ts +0 -2
- package/dist/src/public/channels/slack/interaction-cards.js +1 -1
- package/dist/src/public/channels/slack/interactions.js +1 -1
- package/dist/src/public/channels/slack/open-code-channel.d.ts +30 -0
- package/dist/src/public/channels/slack/open-code-channel.js +1 -0
- package/dist/src/public/channels/slack/private-approval-delivery.d.ts +16 -4
- package/dist/src/public/channels/slack/private-approval-delivery.js +1 -1
- package/dist/src/public/channels/slack/renderers.d.ts +58 -6
- package/dist/src/public/channels/slack/renderers.js +1 -1
- package/dist/src/public/channels/slack/reply-delivery.d.ts +4 -0
- package/dist/src/public/channels/slack/reply-delivery.js +1 -1
- package/dist/src/public/channels/slack/request-card-render.d.ts +45 -0
- package/dist/src/public/channels/slack/request-card-render.js +1 -0
- package/dist/src/public/channels/slack/request-cards.d.ts +35 -0
- package/dist/src/public/channels/slack/request-cards.js +3 -0
- package/dist/src/public/channels/slack/sign-in-cancel.d.ts +3 -5
- package/dist/src/public/channels/slack/sign-in-cancel.js +1 -1
- package/dist/src/public/channels/slack/slackChannel.d.ts +109 -22
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/public/channels/slack/task-card.d.ts +3 -3
- package/dist/src/public/channels/slack/task-card.js +1 -1
- package/dist/src/public/channels/slack/thread-status.js +1 -1
- package/dist/src/public/channels/teams/api.js +1 -1
- package/dist/src/public/channels/teams/defaults.js +1 -1
- package/dist/src/public/channels/teams/teamsChannel.js +1 -1
- package/dist/src/public/channels/teams/verify.d.ts +9 -5
- package/dist/src/public/channels/teams/verify.js +1 -1
- package/dist/src/public/channels/telegram/telegramChannel.js +1 -1
- package/dist/src/public/channels/twilio/twilioChannel.js +1 -1
- package/dist/src/public/schedules/subscription.d.ts +10 -1
- package/dist/src/runtime/subagents/workflow.js +1 -1
- package/dist/src/setup/index.d.ts +1 -1
- package/dist/src/setup/primitives/index.d.ts +1 -1
- package/dist/src/setup/primitives/pm/run.d.ts +0 -2
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/dist/src/shared/channel-definition.d.ts +4 -4
- package/dist/src/subagents/adapter.js +1 -1
- package/dist/src/subagents/parent-notification.js +1 -1
- package/dist/src/svelte/index.js +1 -1
- package/dist/src/svelte/use-eve-agent.js +1 -1
- package/dist/src/tools/definition.d.ts +21 -0
- package/dist/src/tools/provided/ask-question-workflow.js +1 -1
- package/dist/src/tools/provided/sleep-workflow.js +1 -1
- package/dist/src/tools/workflow-definition.d.ts +4 -4
- package/dist/src/vue/index.js +1 -1
- package/dist/src/vue/use-eve-agent.js +1 -1
- package/docs/channels/custom.mdx +16 -3
- package/docs/channels/eve.mdx +2 -0
- package/docs/channels/linq.mdx +1 -1
- package/docs/channels/slack.mdx +185 -18
- package/docs/channels/teams.mdx +6 -2
- package/docs/concepts/upgrade-to-v27-events.md +2 -2
- package/docs/experimental/dynamic-schedules.mdx +2 -2
- package/docs/guides/client/overview.mdx +1 -1
- package/docs/guides/remote-agents.md +8 -0
- package/docs/patterns/fork-conversations-into-new-threads.md +159 -0
- package/docs/patterns/meta.json +1 -0
- package/docs/schedules.mdx +1 -1
- package/docs/tools/overview.mdx +34 -0
- package/docs/tools/workflows.mdx +47 -5
- package/package.json +1 -1
- package/dist/src/public/channels/slack/approval-cards.d.ts +0 -25
- package/dist/src/public/channels/slack/approval-cards.js +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"#internal/logging.js";import{buildSlackBinding}from"#public/channels/slack/api.js";import{buildSlackAuthContext}from"#public/channels/slack/auth.js";const log=createLogger(`slack.interactions`);async function authorizeAnswer(e){let
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{buildSlackBinding}from"#public/channels/slack/api.js";import{buildSlackAuthContext}from"#public/channels/slack/auth.js";const log=createLogger(`slack.interactions`);async function authorizeAnswer(e){let{channelId:t,codeChannel:n,threadTs:r=``}=e.target,i=buildSlackAuthContext({channelId:t,installationTeamId:e.installationTeamId,teamId:e.teamId,threadTs:r,userId:e.submission.user.id,userName:e.submission.user.username??e.submission.user.name}),{thread:a,slack:o}=buildSlackBinding({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,channelId:t,threadTs:r,codeChannel:n,installationTeamId:e.installationTeamId??void 0,teamId:e.teamId??void 0}),s={defaultAuth:i,slack:o,thread:a};try{return await e.deps.onAnswers(s,e.submission)}catch(e){return log.error(`HITL input response authorization failed`,{error:e}),null}}export{authorizeAnswer};
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { type SlackApiResponse as SlackPrimitiveApiResponse } from "#compiled/@chat-adapter/slack/api.js";
|
|
18
18
|
import { type CardElement, type FileUpload } from "#public/channels/slack/cards.js";
|
|
19
19
|
import { type SlackTransportOptions } from "#public/channels/slack/transport.js";
|
|
20
|
+
import type { SlackSessionTarget } from "#public/channels/slack/slackChannel.js";
|
|
20
21
|
/** Slack app installation workspace available when eve resolves a bot token. */
|
|
21
22
|
export interface SlackBotTokenContext {
|
|
22
23
|
readonly teamId?: string;
|
|
@@ -37,6 +38,15 @@ export declare function slackContinuationToken({ channelId, threadTs, }: {
|
|
|
37
38
|
readonly channelId: string;
|
|
38
39
|
readonly threadTs: string;
|
|
39
40
|
}): string;
|
|
41
|
+
/**
|
|
42
|
+
* Builds the channel-local continuation token of a code channel's one
|
|
43
|
+
* session (`code:<channelId>`), which no thread token can collide with.
|
|
44
|
+
*/
|
|
45
|
+
export declare function slackCodeChannelContinuationToken(channelId: string): string;
|
|
46
|
+
/** The session `slack` is bound to: its code channel, or its thread. */
|
|
47
|
+
export declare function slackSessionTargetOf(slack: Pick<SlackHandle, "channelId" | "codeChannel" | "threadTs">): SlackSessionTarget;
|
|
48
|
+
/** The continuation token of a thread's or a code channel's session. */
|
|
49
|
+
export declare function slackSessionToken(target: SlackSessionTarget): string;
|
|
40
50
|
/**
|
|
41
51
|
* Materializes a {@link SlackBotToken} to a string, falling back to
|
|
42
52
|
* `process.env.SLACK_BOT_TOKEN`. Throws when neither is set.
|
|
@@ -224,8 +234,9 @@ export interface SlackThread {
|
|
|
224
234
|
/**
|
|
225
235
|
* Show a typing/status indicator in this thread via Slack's
|
|
226
236
|
* `assistant.threads.setStatus`. Called with no argument, clears the
|
|
227
|
-
* indicator (empty status).
|
|
228
|
-
*
|
|
237
|
+
* indicator (empty status). In a code channel, sets the session status to
|
|
238
|
+
* `processing`, or `active` when cleared. Failures are logged and swallowed:
|
|
239
|
+
* the indicator is a UX nicety, never a reason to fail a turn.
|
|
229
240
|
*/
|
|
230
241
|
startTyping(status?: string): Promise<void>;
|
|
231
242
|
/**
|
|
@@ -251,8 +262,13 @@ export interface SlackThread {
|
|
|
251
262
|
export interface SlackHandle {
|
|
252
263
|
/** Slack channel id. */
|
|
253
264
|
readonly channelId: string;
|
|
254
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* Slack thread root ts (or the message ts when not in a thread). Empty in a
|
|
267
|
+
* code channel, which is one session across its threads.
|
|
268
|
+
*/
|
|
255
269
|
readonly threadTs: string;
|
|
270
|
+
/** Whether the session serves a Slack code channel. */
|
|
271
|
+
readonly codeChannel: boolean;
|
|
256
272
|
/** Slack team id, when the inbound event carried one. */
|
|
257
273
|
readonly teamId: string | undefined;
|
|
258
274
|
/**
|
|
@@ -339,6 +355,10 @@ export declare function buildSlackBinding(input: {
|
|
|
339
355
|
readonly botUserId?: string;
|
|
340
356
|
readonly channelId: string;
|
|
341
357
|
readonly threadTs: string;
|
|
358
|
+
/** Binds the code channel `channelId` as its one session; `threadTs` is ignored. */
|
|
359
|
+
readonly codeChannel?: boolean;
|
|
360
|
+
/** In a code channel, the thread replies post in instead of top-level. */
|
|
361
|
+
readonly replyThreadTs?: () => string | undefined;
|
|
342
362
|
/**
|
|
343
363
|
* Whether `threadTs` already has replies; Slack hides threaded
|
|
344
364
|
* ephemerals until it does. Defaults to true when `threadTs` is set.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger,logError}from"#internal/logging.js";import{fetchSlackThreadReplies,postSlackEphemeral,postSlackMessage,resolveSlackBotToken as resolveSlackBotToken$1,uploadSlackFiles}from"#compiled/@chat-adapter/slack/api.js";import{isCardElement}from"#public/channels/slack/cards.js";import{cardToBlocks,cardToFallbackText}from"#public/channels/slack/blocks.js";import{resolveSlackInboundMrkdwn}from"#public/channels/slack/inbound-content.js";import{truncateTypingStatus}from"#public/channels/slack/limits.js";import{slackMrkdwnToGfm}from"#public/channels/slack/mrkdwn.js";import{callSlackApiTrackingResponse,slackApiOptions}from"#public/channels/slack/transport.js";const log=createLogger(`slack.api`);function slackContinuationToken({channelId:e,threadTs:t}){return`${e}:${t}`}async function resolveSlackBotToken(e,t={}){let n=e??process.env.SLACK_BOT_TOKEN;if(!n)throw Error(`SLACK_BOT_TOKEN is required.`);return typeof n==`function`?n(t):resolveSlackBotToken$1(n)}async function callSlackApi(e){return callSlackApiTrackingResponse(e.operation,normalizeSlackApiBody(e.body),slackApiOptions(e.api,()=>resolveSlackBotToken(e.botToken,e.context)))}function createSlackRequester(e,t,n){return(r,i)=>callSlackApi({api:e,botToken:t,context:n,operation:r,body:i})}function buildSlackWorkspaceHandle(e){return{teamId:e.teamId,request:createSlackRequester(e.api,e.botToken,{teamId:e.installationTeamId})}}const THREAD_REFRESH_LIMIT=50,threadWindowFetchers=new WeakMap;async function fetchSlackThreadWindow(e,t,n){return await threadWindowFetchers.get(e)?.(t,n)}function buildSlackBinding(e){let t={teamId:e.installationTeamId},n=slackApiOptions(e.api,()=>resolveSlackBotToken(e.botToken,t)),r=createSlackRequester(e.api,e.botToken,t),request=async(t,n)=>{let i=await r(t,n);if(t===`chat.postMessage`&&i.ok===!0){let t=n;
|
|
1
|
+
import{createLogger,logError}from"#internal/logging.js";import{fetchSlackThreadReplies,postSlackEphemeral,postSlackMessage,resolveSlackBotToken as resolveSlackBotToken$1,uploadSlackFiles}from"#compiled/@chat-adapter/slack/api.js";import{isCardElement}from"#public/channels/slack/cards.js";import{cardToBlocks,cardToFallbackText}from"#public/channels/slack/blocks.js";import{resolveSlackInboundMrkdwn}from"#public/channels/slack/inbound-content.js";import{setSlackCodeChannelStatus,slackThreadTsParam}from"#public/channels/slack/code-channel.js";import{truncateTypingStatus}from"#public/channels/slack/limits.js";import{slackMrkdwnToGfm}from"#public/channels/slack/mrkdwn.js";import{callSlackApiTrackingResponse,slackApiOptions}from"#public/channels/slack/transport.js";const log=createLogger(`slack.api`);function slackContinuationToken({channelId:e,threadTs:t}){return`${e}:${t}`}function slackCodeChannelContinuationToken(e){return`code:${e}`}function slackSessionTargetOf(e){return e.codeChannel?{channelId:e.channelId,codeChannel:!0}:{channelId:e.channelId,threadTs:e.threadTs}}function slackSessionToken(e){return e.codeChannel===!0?slackCodeChannelContinuationToken(e.channelId):slackContinuationToken({channelId:e.channelId,threadTs:e.threadTs})}async function resolveSlackBotToken(e,t={}){let n=e??process.env.SLACK_BOT_TOKEN;if(!n)throw Error(`SLACK_BOT_TOKEN is required.`);return typeof n==`function`?n(t):resolveSlackBotToken$1(n)}async function callSlackApi(e){return callSlackApiTrackingResponse(e.operation,normalizeSlackApiBody(e.body),slackApiOptions(e.api,()=>resolveSlackBotToken(e.botToken,e.context)))}function createSlackRequester(e,t,n){return(r,i)=>callSlackApi({api:e,botToken:t,context:n,operation:r,body:i})}function buildSlackWorkspaceHandle(e){return{teamId:e.teamId,request:createSlackRequester(e.api,e.botToken,{teamId:e.installationTeamId})}}const THREAD_REFRESH_LIMIT=50,threadWindowFetchers=new WeakMap;async function fetchSlackThreadWindow(e,t,n){return await threadWindowFetchers.get(e)?.(t,n)}function buildSlackBinding(e){let t={teamId:e.installationTeamId},n=slackApiOptions(e.api,()=>resolveSlackBotToken(e.botToken,t)),r=createSlackRequester(e.api,e.botToken,t),request=async(t,n)=>{let i=await r(t,n);if(t===`chat.postMessage`&&i.ok===!0){let t=n;o&&t.channel===e.channelId&&t.thread_ts===o&&e.onPosted?.()}return i},i=[],a=e.codeChannel===!0,o=a?``:e.threadTs,hasSessionThread=()=>a||o!==``,s=e.threadHasReplies??hasSessionThread(),c;function postThreadTs(){return(a?e.replyThreadTs?.():void 0)||o}function handleMessageTs(t){hasSessionThread()||t===o||(o=t,e.onThreadTsChanged?.(t))}async function uploadFiles(t,r){let i=r?.channelId??e.channelId,a=r?.threadTs??postThreadTs(),o=t.map(e=>toSlackFileUpload(e,r?.snippetType)),s=await uploadSlackFiles(o,{...n,channelId:i||void 0,initialComment:r?.initialComment,threadTs:slackThreadTsParam(a)});return i===e.channelId&&hasSessionThread()&&a===postThreadTs()&&e.onPosted?.(),s}function refreshMessages(){if(c!==void 0)return c;async function fetchAndReplace(){if(!e.channelId||!o||a){i=[];return}try{i=(await fetchSlackThreadReplies({...n,channel:e.channelId,limit:50,ts:o})).messages.map(t=>parseThreadMessage(t,o,{appId:e.appId,botUserId:e.botUserId}))}catch(t){logError(log,`refresh threw — swallowed`,t,{channelId:e.channelId})}}let t=fetchAndReplace().finally(()=>{c===t&&(c=void 0)});return c=t,t}async function fetchWindow(t,r){if(!e.channelId||!o||a)return;let i=[],s;for(let a=0;a<5;a+=1){let a=await fetchSlackThreadReplies({...n,channel:e.channelId,cursor:s,inclusive:!0,latest:t,limit:200,ts:o});if(i=[...i,...a.messages].slice(-r),s=a.nextCursor,s===void 0||s===``)return i.map(t=>parseThreadMessage(t,o,{appId:e.appId,botUserId:e.botUserId}))}}let l={get recentMessages(){return i},async listParticipants(){await l.refresh();let e=new Set;for(let t of i)t.user!==void 0&&t.botId===void 0&&e.add(t.user);return[...e]},async post(t){let r=normalizePostInput(t),i=r.files??[],o=`blocks`in r||`card`in r||`markdown`in r;if(i.length>0&&!o){let e=await uploadFiles(i,{initialComment:`text`in r?r.text:void 0});return hasSessionThread()&&(s=!0),{id:Array.isArray(e.raw.files)&&e.raw.files.length>0?String(e.raw.files[0].id??``):``,raw:e.raw}}let c=postThreadTs(),l=await postSlackMessage(buildPostMessageOptions(r,e.channelId,postThreadTs(),n)),u=l.id;if((c||a)&&e.onPosted?.(),hasSessionThread()&&(s=!0),handleMessageTs(u),i.length>0&&o)try{await uploadFiles(i)}catch(e){log.warn(`file upload after message post failed`,{error:e})}return{id:u,raw:l.raw}},async postEphemeral(t,r){let i=normalizePostInput(r),a=s?postThreadTs():``,o=await postSlackEphemeral({...buildPostMessageOptions(i,e.channelId,a,n),user:t});return{id:o.id,raw:o.raw}},async postDirectMessage(e,t){let r=await request(`conversations.open`,{users:e}),i=r.ok===!0?r.channel?.id:void 0;if(typeof i!=`string`||i.length===0)throw Error(`Slack conversations.open failed: ${r.error??`unknown_error`}`);let a=normalizePostInput(t),o=await postSlackMessage(buildPostMessageOptions(a,i,``,n));return{id:o.id,raw:o.raw}},async startTyping(t){if(e.channelId){if(a){await setSlackCodeChannelStatus(request,e.channelId,t?`processing`:`active`);return}if(o)try{let n=t===void 0?``:truncateTypingStatus(t),r={channel_id:e.channelId,thread_ts:o,status:n};n.length>0&&(r.loading_messages=[n]);let i=await request(`assistant.threads.setStatus`,r);i.ok!==!0&&log.warn(`assistant.threads.setStatus returned not-ok`,{error:i.error})}catch(t){logError(log,`startTyping threw — swallowed`,t,{channelId:e.channelId})}}},refresh(){return refreshMessages()},mentionUser(e){return`<@${e}>`}},u={channelId:e.channelId,get threadTs(){return o},codeChannel:a,teamId:e.teamId,request,uploadFiles};return threadWindowFetchers.set(l,fetchWindow),{thread:l,slack:u}}function normalizePostInput(e){return typeof e==`string`?{markdown:e}:isCardElement(e)?{card:e}:e}function buildPostMessageOptions(e,t,n,r){let i={...r,channel:t,threadTs:slackThreadTsParam(n),unfurlLinks:!1,unfurlMedia:!1};return`card`in e?(i.blocks=cardToBlocks(e.card),i.text=e.fallbackText??cardToFallbackText(e.card),i):`blocks`in e?(i.blocks=[...e.blocks],e.text!==void 0&&(i.text=e.text),i):`markdown`in e?(i.markdownText=e.markdown,i):(i.text=e.text,i)}function normalizeSlackApiBody(e){return e&&typeof e==`object`&&!Array.isArray(e)?e:{}}function toSlackFileUpload(e,t){return{data:normalizeFileData(e.data),filename:e.filename,snippetType:t}}function normalizeFileData(e){if(e instanceof ArrayBuffer||typeof Blob<`u`&&e instanceof Blob)return e;if(ArrayBuffer.isView(e)){let t=e;return new Uint8Array(t.buffer,t.byteOffset,t.byteLength)}return e}function parseThreadMessage(e,t,n){let r=typeof e.text==`string`?e.text:``,i=resolveSlackInboundMrkdwn(r,e),a=typeof e.ts==`string`?e.ts:``,o=typeof e.thread_ts==`string`?e.thread_ts:t,s=typeof e.user==`string`?e.user:void 0,c=typeof e.bot_id==`string`?e.bot_id:void 0,l=typeof e.app_id==`string`?e.app_id:void 0,u=n.botUserId!==void 0&&s===n.botUserId||n.appId!==void 0&&l===n.appId;return{text:i,markdown:slackMrkdwnToGfm(i),user:s,botId:c,ts:a,threadTs:o,isMe:u,raw:e}}export{THREAD_REFRESH_LIMIT,buildSlackBinding,buildSlackWorkspaceHandle,callSlackApi,fetchSlackThreadWindow,resolveSlackBotToken,slackCodeChannelContinuationToken,slackContinuationToken,slackSessionTargetOf,slackSessionToken};
|
|
@@ -4,5 +4,7 @@ interface SlackAppManifestBuildDefinition {
|
|
|
4
4
|
}
|
|
5
5
|
export declare function defineSlackAppManifest(input: {
|
|
6
6
|
readonly botName?: string;
|
|
7
|
+
/** Adds the scopes, events, and feature flag Slack code channels need. */
|
|
8
|
+
readonly codeChannels?: boolean;
|
|
7
9
|
}): SlackAppManifestBuildDefinition;
|
|
8
10
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function defineSlackAppManifest(e){return{build(t){let n=(e.botName??t).slice(0,35)
|
|
1
|
+
function defineSlackAppManifest(e){return{build(t){let n=(e.botName??t).slice(0,35),r={$type:`https://docs.slack.dev/reference/app-manifest/`,display_information:{name:n},features:{app_home:{home_tab_enabled:!1,messages_tab_enabled:!0,messages_tab_read_only_enabled:!1},bot_user:{display_name:n}},oauth_config:{scopes:{bot:[`app_mentions:read`,`chat:write`]}},settings:{event_subscriptions:{bot_events:[`app_mention`]},org_deploy_enabled:!1,socket_mode_enabled:!1,token_rotation_enabled:!1}};return e.codeChannels===!0?withCodeChannels(r):r}}}function withCodeChannels(e){return{...e,features:{...e.features,code_channels:{enabled:!0}},oauth_config:{scopes:{bot:[...e.oauth_config.scopes.bot,`channels:history`,`channels:read`,`code_channels:manage`,`groups:history`,`groups:read`,`im:read`,`mpim:read`]}},settings:{...e.settings,event_subscriptions:{bot_events:[...e.settings.event_subscriptions.bot_events,`agent_session_stopped`,`message.session`]}}}}export{defineSlackAppManifest};
|
|
@@ -7,7 +7,8 @@ interface SlackAuthContextInput {
|
|
|
7
7
|
readonly installationTeamId?: string | null;
|
|
8
8
|
readonly isBot?: boolean;
|
|
9
9
|
readonly teamId?: string | null;
|
|
10
|
-
|
|
10
|
+
/** Thread root ts; omit it in a code channel, which is one session across its threads. */
|
|
11
|
+
readonly threadTs?: string;
|
|
11
12
|
readonly userId: string;
|
|
12
13
|
readonly userName?: string;
|
|
13
14
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function slackUserIdFromAuthContext(e){if(e?.authenticator!==`slack-webhook`)return;let t=e.attributes.user_id;return typeof t==`string`&&t.length>0?t:void 0}function slackUserIdForPrincipal(e,t){return t===void 0?void 0:e.slackUsersByPrincipal?.[t]}function buildSlackAuthContext(e){let t=e.isBot===!0,n=e.installationTeamId||e.teamId,r=n?t?`slack:${n}:bot:${e.userId}`:`slack:${n}:${e.userId}`:t?`slack:bot:${e.userId}`:`slack:${e.userId}`,i={author_type:t?`bot`:`user`,channel_id:e.channelId,
|
|
1
|
+
function slackUserIdFromAuthContext(e){if(e?.authenticator!==`slack-webhook`)return;let t=e.attributes.user_id;return typeof t==`string`&&t.length>0?t:void 0}function slackUserIdForPrincipal(e,t){return t===void 0?void 0:e.slackUsersByPrincipal?.[t]}function buildSlackAuthContext(e){let t=e.isBot===!0,n=e.installationTeamId||e.teamId,r=n?t?`slack:${n}:bot:${e.userId}`:`slack:${n}:${e.userId}`:t?`slack:bot:${e.userId}`:`slack:${e.userId}`,i={author_type:t?`bot`:`user`,channel_id:e.channelId,user_id:e.userId};return e.threadTs&&(i.thread_ts=e.threadTs),e.userName&&(i.user_name=e.userName),e.fullName&&(i.full_name=e.fullName),e.teamId&&(i.team_id=e.teamId),{attributes:i,authenticator:`slack-webhook`,issuer:n?`slack:${n}`:`slack`,principalId:r,principalType:t?`service`:`user`}}export{buildSlackAuthContext,slackUserIdForPrincipal,slackUserIdFromAuthContext};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { SlackApiResponse } from "#public/channels/slack/api.js";
|
|
2
|
+
export type SlackRequest = (operation: string, body: unknown) => Promise<SlackApiResponse>;
|
|
3
|
+
/**
|
|
4
|
+
* Whether the conversation is a code channel. A failed lookup counts as not a
|
|
5
|
+
* code channel, so its messages get per-thread sessions until a retry
|
|
6
|
+
* succeeds a minute later.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSlackCodeChannel(channelId: string, request: SlackRequest): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* Opens a code channel from an origin message. Slack invites the origin
|
|
11
|
+
* message's author, links the channel in the origin thread, and refuses Slack
|
|
12
|
+
* Connect origins. `session_id` makes the call idempotent, so a retry with the
|
|
13
|
+
* same origin and requester returns the same channel. Throws an error marked
|
|
14
|
+
* `fatal` when Slack refuses permanently, so the calling step doesn't retry.
|
|
15
|
+
*
|
|
16
|
+
* eve reads the origin before creating anything and asks for a private
|
|
17
|
+
* channel when the origin is private. Slack always honors an explicit
|
|
18
|
+
* `is_private`, so the check after creation is a backstop: on a mismatch eve
|
|
19
|
+
* archives the channel and throws.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createSlackCodeChannel(input: {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly originChannelId: string;
|
|
24
|
+
readonly originTs: string;
|
|
25
|
+
/** Principal the channel is opened for; one channel per requester and origin message. */
|
|
26
|
+
readonly requester: string;
|
|
27
|
+
readonly request: SlackRequest;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
readonly channelId: string;
|
|
30
|
+
readonly isPrivate: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* Whether a message is the origin quote Slack posts when it creates a code
|
|
34
|
+
* channel from a message. It is context, not a request.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isSlackCodeChannelOriginContext(raw: Record<string, unknown>): boolean;
|
|
37
|
+
/** Slack agent session lifecycle states. */
|
|
38
|
+
export type SlackSessionStatus = "active" | "processing" | "suspended";
|
|
39
|
+
/** Sets a code channel's session status. Failures are logged, never thrown. */
|
|
40
|
+
export declare function setSlackCodeChannelStatus(request: SlackRequest, channelId: string, status: SlackSessionStatus): Promise<void>;
|
|
41
|
+
/** Slack's `thread_ts` for a session thread: omitted for top-level posts. */
|
|
42
|
+
export declare function slackThreadTsParam(threadTs: string): string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash}from"node:crypto";import{createLogger,logError}from"#internal/logging.js";const log=createLogger(`slack.code-channel`),codeChannelCache=new Map;async function isSlackCodeChannel(e,t){let n=codeChannelCache.get(e);if(typeof n==`boolean`)return n;if(n!==void 0&&Date.now()-n.failedAtMs<6e4)return!1;let r;try{let n=await t(`conversations.info`,{channel:e});if(n.ok!==!0)return log.error(n.error===`missing_scope`?`conversations.info needs the channels:read and groups:read scopes to detect code channels; reinstall the Slack app`:`conversations.info failed; treating the channel as an ordinary channel`,{channelId:e,error:n.error}),cacheLookup(e,{failedAtMs:Date.now()}),!1;r=n.channel?.properties?.record_channel?.record_type===`agent_channel`}catch(t){return logError(log,`conversations.info threw; treating the channel as an ordinary channel`,t,{channelId:e}),cacheLookup(e,{failedAtMs:Date.now()}),!1}return cacheLookup(e,r),r}function cacheLookup(e,t){codeChannelCache.delete(e),codeChannelCache.size>=1e3&&codeChannelCache.delete(codeChannelCache.keys().next().value),codeChannelCache.set(e,t)}const TRANSIENT_SLACK_ERRORS=new Set([`channel_creation_failed`,`fatal_error`,`internal_error`,`invite_failed`,`open_failed`,`ratelimited`,`request_timeout`,`service_unavailable`]);async function createSlackCodeChannel(e){let t=createHash(`sha256`).update(`${e.originChannelId}:${e.originTs}:${e.requester}`).digest(`hex`).slice(0,32),n=e.name.replace(/\s+/gu,` `).trim().slice(0,80),r=isPrivateConversation(await readConversation(e.request,e.originChannelId)),i=await e.request(`agents.conversations.create`,{...n.length>0&&{name:n},...r&&{is_private:!0},origin_channel_id:e.originChannelId,origin_message_ts:e.originTs,session_id:t});if(i.ok!==!0)throw slackFailure(`agents.conversations.create`,i.error);let a=i.channel_id;if(typeof a!=`string`||a.length===0)throw Error(`Slack did not return a channel id for the new code channel.`);if(cacheLookup(a,!0),r&&!isPrivateConversation(await readConversation(e.request,a)))throw fatalError(`Slack opened code channel ${a} as public from a private conversation, so eve ${(await e.request(`agents.conversations.archive`,{channel_id:a})).ok===!0?`archived it`:`couldn't archive it`} and didn't copy the conversation into it.`);return{channelId:a,isPrivate:r}}async function readConversation(e,t){let n=await e(`conversations.info`,{channel:t});if(n.ok!==!0)throw slackFailure(`conversations.info`,n.error);return n.channel??{}}function isPrivateConversation(e){return e.is_private===!0||e.is_im===!0||e.is_mpim===!0}function slackFailure(e,t){let n=typeof t==`string`?t:`unknown_error`,r=`Slack ${e} failed: ${n}`;return TRANSIENT_SLACK_ERRORS.has(n)?Error(r):fatalError(r)}function fatalError(e){return Object.assign(Error(e),{fatal:!0})}function isSlackCodeChannelOriginContext(e){let t=e.blocks;return Array.isArray(t)&&t.some(e=>e?.block_id===`agent_channel_origin_context`)}async function setSlackCodeChannelStatus(e,t,n){try{let r=await e(`agents.sessions.setStatus`,{channel_id:t,status:n});r.ok!==!0&&log.warn(`agents.sessions.setStatus returned not-ok`,{error:r.error,status:n})}catch(e){logError(log,`agents.sessions.setStatus threw`,e,{channelId:t})}}function slackThreadTsParam(e){return e===``?void 0:e}export{createSlackCodeChannel,isSlackCodeChannel,isSlackCodeChannelOriginContext,setSlackCodeChannelStatus,slackThreadTsParam};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionAuthContext } from "#channel/types.js";
|
|
2
2
|
import type { SlackMessage } from "#public/channels/slack/inbound.js";
|
|
3
|
+
import type { SlackInteractionCardChain } from "#public/channels/slack/renderers.js";
|
|
3
4
|
import { type SignIn } from "#channel/sign-in.js";
|
|
4
5
|
import type { SlackApprovalChannelResolver, SlackChannelInternalEvents, SlackContext, SlackEventContext, SlackMentionResult } from "#public/channels/slack/slackChannel.js";
|
|
5
6
|
/**
|
|
@@ -24,10 +25,13 @@ export declare function defaultReceived(_message: SlackMessage, ctx: SlackContex
|
|
|
24
25
|
*/
|
|
25
26
|
export declare function showSlackSignIn(event: SignIn, channel: SlackEventContext): Promise<void>;
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
* as
|
|
28
|
+
* eve's default handlers for input requests, rendering cards through the composed
|
|
29
|
+
* `interactionCard` chain. A commit's requests post as one batch on its first
|
|
30
|
+
* `interaction.opened`. Each card shows an open request's answer when it is admitted or refused,
|
|
31
|
+
* and its settled rendering on `interaction.settled`. A sign-in renders as its public status and
|
|
32
|
+
* private challenge instead.
|
|
29
33
|
*/
|
|
30
|
-
export declare function
|
|
34
|
+
export declare function defaultInteractionEvents(approvalChannel: SlackApprovalChannelResolver | undefined, interactionCard: SlackInteractionCardChain): Pick<SlackChannelInternalEvents, "interaction.opened" | "interaction.settled" | "response.admitted" | "response.settled">;
|
|
31
35
|
/**
|
|
32
36
|
* eve's default Slack event rendering: status lines, replies, errors, and the
|
|
33
37
|
* connection-authorization flow. It is the innermost link of every channel's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{contentPhase}from"#protocol/session-events/catalog.js";import{callLabel,waiting}from"#protocol/session-projection/selectors.js";import{createLogger,formatErrorHint}from"#internal/logging.js";import{signInOf}from"#channel/sign-in.js";import{refusedAnswerOf,requestBatchOf
|
|
1
|
+
import{contentPhase}from"#protocol/session-events/catalog.js";import{callLabel,waiting}from"#protocol/session-projection/selectors.js";import{createLogger,formatErrorHint,logError}from"#internal/logging.js";import{signInOf}from"#channel/sign-in.js";import{refusedAnswerOf,requestBatchOf}from"#channel/interaction-prompts.js";import{sessionTables}from"#protocol/session-projection/session-view.js";import{errorHintOf}from"#public/channels/reply.js";import{slackSessionTargetOf}from"#public/channels/slack/api.js";import{buildSlackAuthContext,slackUserIdForPrincipal}from"#public/channels/slack/auth.js";import{truncateTypingStatus}from"#public/channels/slack/limits.js";import{workingTaskNames}from"#channel/task-card.js";import{reviewingResults,waitingOnTasks,withMoreCalls}from"#public/channels/slack/action-status.js";import{buildAuthCompletedText,buildAuthEphemeralBlocks,buildAuthEphemeralText,buildAuthRequiredPublicText}from"#public/channels/slack/connections.js";import{notifyRefusedResponder,postRequestCards,settleRequestCard,showAnswerChange}from"#public/channels/slack/request-cards.js";import{deliverCompletedSlackReply}from"#public/channels/slack/reply-delivery.js";import{clearStatus,hideStatus,reasoningStatus,showStatus}from"#public/channels/slack/thread-status.js";const log=createLogger(`slack.defaults`),streamByState=new WeakMap;function stepStream(e,t){let n=streamByState.get(e);if(n?.runId===t)return n;let r={calls:null,partKinds:new Map,reasoning:``,reasoningShownAtMs:null,replyChars:0,runId:t};return streamByState.set(e,r),r}function deltaStream(e,t,n){if(n!==void 0&&t.kind!==void 0){let r=stepStream(e,n);return r.partKinds.set(t.partId,t.kind),{kind:t.kind,stream:r}}let r=streamByState.get(e),i=r?.partKinds.get(t.partId);return r===void 0||i===void 0?void 0:{kind:i,stream:r}}function heldWithin(e,t){return e!=null&&t-e>=0&&t-e<3e3}async function showReasoning(e,t,n){let r=reasoningStatus(t.reasoning,n);if(r===void 0||truncateTypingStatus(r)===e.state.threadStatus?.text)return;let i=Date.now();heldWithin(t.reasoningShownAtMs,i)||(await showStatus(e,r),t.reasoningShownAtMs=i)}function formatFixableErrorReply(e,t,n,r){let i=[t.message,``,`**How to fix**`,n,...t.id===void 0?[]:[``,`**Error id:**`,`\`${t.id}\``]].flatMap(e=>e.split(`
|
|
2
2
|
`)).map(e=>e.length>0?`> ${e}`:`> `).join(`
|
|
3
3
|
`);return[`${e}.`,``,i,...r?.followUp===void 0?[]:[``,r.followUp]].join(`
|
|
4
|
-
`)}function defaultSlackAuth(e,t){let n=e.author;return n?buildSlackAuthContext({channelId:t.slack.channelId,fullName:n.fullName,installationTeamId:e.installationTeamId,isBot:n.isBot,teamId:e.teamId,threadTs:t.slack.threadTs,userId:n.userId,userName:n.userName}):null}function defaultOnMessage(e,t){return{auth:defaultSlackAuth(t,e)}}async function defaultReceived(e,t){await t.thread.startTyping(`Thinking...`)}function firstNonEmptyLine(e){for(let t of e.split(/\r?\n/u)){let e=t.trim();if(e.length>0)return e}}async function showSlackSignIn(e,t){let{displayName:n}=e,r=slackUserIdForPrincipal(t.state,e.principalId)??null,i=e.url,a=t.state.pendingAuthMessageTs??{};if(e.responseId===void 0&&a[e.name]===void 0){let i=buildAuthRequiredPublicText({displayName:n,recipientUserId:r});try{let n=await t.thread.post(i);n.id&&(t.state.pendingAuthMessageTs={...a,[e.name]:n.id})}catch(t){log.error(`Slack auth public message delivery failed`,{name:e.name,error:t})}}let{instructions:o}=e;if(r&&(i||o)){let
|
|
4
|
+
`)}function defaultSlackAuth(e,t){let n=e.author;return n?buildSlackAuthContext({channelId:t.slack.channelId,fullName:n.fullName,installationTeamId:e.installationTeamId,isBot:n.isBot,teamId:e.teamId,threadTs:t.slack.threadTs,userId:n.userId,userName:n.userName}):null}function defaultOnMessage(e,t){return{auth:defaultSlackAuth(t,e)}}async function defaultReceived(e,t){await t.thread.startTyping(`Thinking...`)}function firstNonEmptyLine(e){for(let t of e.split(/\r?\n/u)){let e=t.trim();if(e.length>0)return e}}async function showSlackSignIn(e,t){let{displayName:n}=e,r=slackUserIdForPrincipal(t.state,e.principalId)??null,i=e.url,a=t.state.pendingAuthMessageTs??{};if(e.responseId===void 0&&a[e.name]===void 0){let i=buildAuthRequiredPublicText({displayName:n,recipientUserId:r});try{let n=await t.thread.post(i);n.id&&(t.state.pendingAuthMessageTs={...a,[e.name]:n.id})}catch(t){log.error(`Slack auth public message delivery failed`,{name:e.name,error:t})}}let{instructions:o}=e;if(r&&(i||o)){let a=slackSessionTargetOf(t.slack),s={cancel:e.responseId===void 0&&a.channelId&&(a.codeChannel||a.threadTs)&&e.turnId?{...a,turnId:e.turnId}:void 0,displayName:n,instructions:o,url:i,userCode:e.userCode};try{await t.thread.postEphemeral(r,{blocks:buildAuthEphemeralBlocks(s),text:buildAuthEphemeralText(s)})}catch(t){log.error(`Slack auth ephemeral delivery failed`,{name:e.name,error:t})}}}async function settleSlackSignIn(e,t,n){let{displayName:r}=e;t.outcome===`accepted`&&e.responseId===void 0&&await showStatus(n,`Connected to ${r}. Resuming...`,{force:!0});let i=n.state.pendingAuthMessageTs??{},a=i[e.name];if(a===void 0)return;let o=buildAuthCompletedText({displayName:r,outcome:t.outcome,reason:t.reason});try{await n.slack.request(`chat.update`,{channel:n.slack.channelId,ts:a,text:o})}catch(t){log.error(`Slack auth status edit failed`,{name:e.name,error:t})}let s={...i};delete s[e.name],n.state.pendingAuthMessageTs=s}function defaultInteractionEvents(e,t){return{async"interaction.opened"({data:n},r){let i=sessionTables(r.view),a=requestBatchOf(i,n);if(a!==void 0){await postRequestCards(a,r,t,e);return}let o=signInOf(i,n.interactionId);o!==void 0&&await showSlackSignIn(o,r.channel)},async"response.admitted"({data:e},n){let r=sessionTables(n.view).responses[e.responseId];r!==void 0&&await showAnswerChange(r.interactionId,n,t)},async"response.settled"({data:e},n){let r=sessionTables(n.view),i=r.responses[e.responseId];i!==void 0&&await showAnswerChange(i.interactionId,n,t);let a=refusedAnswerOf(r,e);if(a!==void 0)try{await notifyRefusedResponder(a,n.channel)}catch(e){logError(log,`failed to tell a refused responder why`,e,{interactionId:a.interactionId})}},async"interaction.settled"({data:e},n){let r=signInOf(sessionTables(n.view),e.interactionId);if(r!==void 0){await settleSlackSignIn(r,e,n.channel);return}await settleRequestCard(e.interactionId,n,t)}}}const defaultEvents={async"turn.paused"(e,{channel:t}){await hideStatus(t)},async"model.settled"(e,t){let n=e.scope?.turnId;if(n===void 0||waiting(sessionTables(t.view),n)?.on!==`work`)return;let r=workingTaskNames(sessionTables(t.view),n);r.length>0&&await showStatus(t.channel,waitingOnTasks(r))},async"model.started"(e,t){let{channel:n}=t,r=e.scope?.turnId;if(r===void 0)return;let i=n.state.pendingTaskResults;n.state.pendingTaskResults=null;let a=i?.turnId===r?reviewingResults(i.names):n.state.threadStatus?.text??`Thinking...`;await showStatus(n,a,{force:!0})},async"turn.started"(e,{channel:t}){t.state.pendingTaskResults=null,t.state.pendingToolCallMessage=null,streamByState.delete(t.state),await showStatus(t,`Thinking...`,{force:!0})},async"content.delta"(e,t){let{data:n}=e,{channel:r}=t,i=deltaStream(r.state,n,e.scope?.runId);if(i===void 0)return;let{kind:a,stream:o}=i;if(a===`reasoning`){o.reasoning===``&&(o.reasoningShownAtMs=null),o.reasoning+=n.delta,await showReasoning(r,o);return}if(a!==`text`)return;let s=o.replyChars;o.replyChars+=n.delta.length,s<280&&o.replyChars>=280&&await showStatus(r,`Writing a reply...`)},async"call.requested"({data:e},{channel:t}){let n=t.state.pendingToolCallMessage;t.state.pendingToolCallMessage=null;let{capability:r,owner:i}=e,a=!(`callId`in i)&&r.kind!==`control`;if(!n&&!a||!(`runId`in i))return;let o=stepStream(t.state,i.runId),s=callLabel(e),c=!n&&o.calls?{...o.calls,count:o.calls.count+1}:{count:+!!a,label:n??s,narrated:n!=null};o.calls=c,await showStatus(t,c.narrated?c.label:withMoreCalls(c.label,c.count))},async"call.progress"({data:e},{channel:t}){let n=e.label;n&&!heldWithin(t.state.threadStatus?.atMs,Date.now())&&await showStatus(t,n)},async"call.settled"(e,t){let{data:n}=e,{channel:r}=t,{taskId:i,turnId:a}=e.scope??{};if(i!==void 0&&a!==void 0&&n.outcome!==`interrupted`){let e=sessionTables(t.view).tasks[i],n=r.state.pendingTaskResults,o=n?.turnId===a?n.names:[],s=e?.kind===`agent`?e.name:null;r.state.pendingTaskResults={names:[...o,s],turnId:a}}n.label&&await showStatus(r,n.label)},async"content.completed"({data:e},t){let{channel:n}=t;if(e.kind===`reasoning`){let t=streamByState.get(n.state);if(t?.runId!==e.runId||typeof e.value!=`string`)return;t.reasoning=e.value,await showReasoning(n,t,{complete:!0}),t.reasoning=``;return}if(e.kind!==`text`)return;let r=typeof e.value==`string`?e.value:``;if(contentPhase(e.phase)===`narration`){n.state.pendingToolCallMessage=r?firstNonEmptyLine(r)??null:null;return}if(n.state.pendingToolCallMessage=null,!r){await clearStatus(n);return}n.state.threadStatus=null,await deliverCompletedSlackReply(n,r,{turnId:t.session.turn.id})},async"turn.settled"({data:e},{channel:t}){if(e.outcome!==`failed`){await clearStatus(t);return}t.state.codeChannel===!0&&await clearStatus(t);let n=e.error?.hint;if(e.error!==void 0&&n!==void 0){await t.thread.post(formatFixableErrorReply(`I hit an error while handling your request`,e.error,n));return}let r=e.error?.id,i=formatErrorHint(errorHintOf(e.error));await t.thread.post([`I hit an error while handling your request${i}.`,``,`Please try again, rephrase, or reach out if it keeps failing.`,...r?[``,`_Error id: \`${r}\`_`]:[]].join(`
|
|
5
5
|
`))},async"session.ended"({data:e},{channel:t}){if(e.outcome!==`failed`)return;let n=e.error?.hint;if(e.error!==void 0&&n!==void 0){await t.thread.post(formatFixableErrorReply(`This session couldn't recover from an error`,e.error,n,{followUp:`Start a new thread to continue — I can't pick this one back up.`}));return}let r=e.error?.id,i=formatErrorHint(errorHintOf(e.error));await t.thread.post([`This session couldn't recover from an error${i}.`,``,`Start a new thread to continue — I can't pick this one back up.`,...r?[``,`_Error id: \`${r}\`_`]:[]].join(`
|
|
6
|
-
`))}};export{defaultEvents,
|
|
6
|
+
`))}};export{defaultEvents,defaultInteractionEvents,defaultOnMessage,defaultReceived,defaultSlackAuth,showSlackSignIn};
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* UX grounds without changing the read path.
|
|
13
13
|
*/
|
|
14
14
|
import type { BlockKitBlock } from "#public/channels/slack/blocks.js";
|
|
15
|
+
import type { SlackInteractionCardParts } from "#public/channels/slack/renderers.js";
|
|
15
16
|
import { type Answer, type ValidatedAnswer } from "#shared/input.js";
|
|
16
17
|
import type { Interaction } from "#channel/interaction-prompts.js";
|
|
18
|
+
import type { SlackSessionTarget } from "#public/channels/slack/slackChannel.js";
|
|
17
19
|
/**
|
|
18
20
|
* Wire-format prefix every framework HITL widget mints onto its
|
|
19
21
|
* `action_id`. Exposed so end-user adapters that render their own
|
|
@@ -42,11 +44,8 @@ export declare const HITL_FREEFORM_MODAL_BLOCK_ID = "eve_freeform_block";
|
|
|
42
44
|
* `action_id` of the text input inside the freeform answer modal.
|
|
43
45
|
*/
|
|
44
46
|
export declare const HITL_FREEFORM_MODAL_ACTION_ID = "eve_freeform_text";
|
|
45
|
-
/** Slack
|
|
46
|
-
export
|
|
47
|
-
readonly channelId: string;
|
|
48
|
-
readonly threadTs: string;
|
|
49
|
-
}
|
|
47
|
+
/** Slack session a routed HITL widget answers back to. */
|
|
48
|
+
export type SlackHitlRoute = SlackSessionTarget;
|
|
50
49
|
/**
|
|
51
50
|
* Subset of one Slack interactivity action the HITL decoder reads.
|
|
52
51
|
* Mirrors the relevant fields of `SlackInteractionAction`.
|
|
@@ -114,19 +113,12 @@ export declare function decodeHitlActionId(actionId: string): DecodedHitlActionI
|
|
|
114
113
|
* Always emits at least the prompt section.
|
|
115
114
|
*/
|
|
116
115
|
export declare function renderInteractionBlocks(request: Interaction, route?: SlackHitlRoute): BlockKitBlock[];
|
|
117
|
-
export interface SlackInteractionPostPart {
|
|
118
|
-
readonly blocks: unknown[];
|
|
119
|
-
readonly text: string;
|
|
120
|
-
}
|
|
121
116
|
/**
|
|
122
117
|
* Splits approval details from their interactive controls. Slack includes the
|
|
123
118
|
* originating message in `block_actions`, so putting large tool input beside
|
|
124
119
|
* the buttons makes the callback body grow with model-authored input.
|
|
125
120
|
*/
|
|
126
|
-
export declare function renderInteractionPostParts(request: Interaction, route?: SlackHitlRoute):
|
|
127
|
-
readonly controls: SlackInteractionPostPart;
|
|
128
|
-
readonly details?: SlackInteractionPostPart;
|
|
129
|
-
};
|
|
121
|
+
export declare function renderInteractionPostParts(request: Interaction, route?: SlackHitlRoute): SlackInteractionCardParts;
|
|
130
122
|
/**
|
|
131
123
|
* Creates the fallback text for one HITL request. Slack clients use this
|
|
132
124
|
* outside the rich Block Kit surface, so include the same approval details
|
|
@@ -139,17 +131,15 @@ export declare function formatInteractionFallbackText(request: Interaction): str
|
|
|
139
131
|
* the modal to the `view_submission` that closes it so the route can
|
|
140
132
|
* deliver the answer back to the right session.
|
|
141
133
|
*/
|
|
142
|
-
export
|
|
134
|
+
export type HitlFreeformModalMetadata = SlackSessionTarget & {
|
|
143
135
|
readonly continuationToken: string;
|
|
144
|
-
readonly channelId: string;
|
|
145
136
|
/** Workspace whose app installation supplies credentials for modal follow-up API calls. */
|
|
146
137
|
readonly installationTeamId?: string;
|
|
147
|
-
readonly threadTs: string;
|
|
148
138
|
/** Slack conversation containing the interactive card. */
|
|
149
139
|
readonly messageChannelId?: string;
|
|
150
140
|
readonly messageTs: string;
|
|
151
141
|
readonly requestId: string;
|
|
152
|
-
}
|
|
142
|
+
};
|
|
153
143
|
/**
|
|
154
144
|
* Builds the `views.open` payload for the freeform-answer modal. The
|
|
155
145
|
* triggering `prompt` is preserved as a header section so the user can
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{parseAnswer}from"#shared/input.js";import{truncate}from"#public/channels/text.js";import{SLACK_SECTION_TEXT_MAX_LENGTH,truncateCardBodyText,truncateModalTitle,truncatePlainText,truncateSectionText}from"#public/channels/slack/limits.js";const ANSWER_ACTION_PREFIX=`eve_input:`,HITL_FREEFORM_ACTION_PREFIX=`eve_input_freeform:`,HITL_FREEFORM_MODAL_CALLBACK_ID=`eve_input_freeform_submit`,HITL_FREEFORM_MODAL_BLOCK_ID=`eve_freeform_block`,HITL_FREEFORM_MODAL_ACTION_ID=`eve_freeform_text`,HITL_ROUTE_PREFIX=`${ANSWER_ACTION_PREFIX}route:`,BUTTON_ACTION_ID_RE=/^(?:(?<kind>tool-approval):)?(?<requestId>.+):button:\d+$/u,TOOL_INPUT_SUFFIX="\n```",answerFromSlackAction=e=>decodeHitlResponse(e);function decodeHitlResponse(e){let t=decodeHitlActionId(e.actionId);if(t===null)return null;let n=e.selectedOptionValue??e.value;if(n===void 0||e.value!==void 0&&!t.button)return null;let r={response:parseAnswer({interactionId:t.requestId,optionId:n})};return t.kind!==void 0&&(r.kind=t.kind),t.route!==void 0&&(r.route=t.route),r}function splitEncodedRequest(e){return e.startsWith(`tool-approval:`)?{kind:`tool-approval`,requestId:e.slice(14)}:{requestId:e}}function isHitlAction(e){return e.startsWith(ANSWER_ACTION_PREFIX)}function decodeHitlActionId(e){if(!e.startsWith(`eve_input:`))return null;let t=e.slice(10),n;if(t.startsWith(`route:`)){t=t.slice(6);let e=t.indexOf(`:`),r=t.indexOf(`:`,e+1);if(e<=0||r<=e+1)return null;n={channelId:t.slice(0,e),threadTs:t.slice(e+1,r)},t=t.slice(r+1)}let r=BUTTON_ACTION_ID_RE.exec(t),i=splitEncodedRequest(r?.groups?.requestId??t);if(!i.requestId)return null;let a=r?.groups?.kind===`tool-approval`?`tool-approval`:i.kind,o={button:r!==null,requestId:i.requestId};return a!==void 0&&(o.kind=a),n!==void 0&&(o.route=n),o}function encodeHitlActionId(e,t){let n=`${e.request.kind===`approval`?`tool-approval:`:``}${e.interactionId}`;return t===void 0?`${ANSWER_ACTION_PREFIX}${n}`:`${HITL_ROUTE_PREFIX}${t.channelId}:${t.threadTs}:${n}`}function renderInteractionBlocks(e,t){let n={text:{text:truncateSectionText(e.request.prompt),type:`mrkdwn`},type:`section`},r=renderInteractionDetailBlocks(e),i=encodeHitlActionId(e,t),a=e.request.options,o=e.request.allowFreeform===!0||!a||a.length===0;if(a&&a.length>0&&e.request.display===`select`){let e=a.length<=6?{type:`radio_buttons`,action_id:i,options:a.map(buildOption)}:{type:`static_select`,action_id:i,options:a.map(buildOption),placeholder:{type:`plain_text`,text:`Choose an option`}};return[n,...r,{type:`actions`,elements:[e]}]}if(a&&a.length>0){let t=renderInteractionCardBlock(e,i),n=renderToolInputContainerBlock(e);return n===void 0?[t]:[t,n]}if(o){let t=i.replace(ANSWER_ACTION_PREFIX,HITL_FREEFORM_ACTION_PREFIX);return[n,...r,{type:`actions`,elements:[{type:`button`,action_id:t,text:{type:`plain_text`,text:`Type your answer`},style:`primary`,value:e.interactionId}]}]}return[n]}function renderInteractionPostParts(e,t){let n=renderInteractionBlocks(e,t),r=n
|
|
1
|
+
import{parseAnswer}from"#shared/input.js";import{truncate}from"#public/channels/text.js";import{SLACK_SECTION_TEXT_MAX_LENGTH,truncateCardBodyText,truncateModalTitle,truncatePlainText,truncateSectionText}from"#public/channels/slack/limits.js";const ANSWER_ACTION_PREFIX=`eve_input:`,HITL_FREEFORM_ACTION_PREFIX=`eve_input_freeform:`,HITL_FREEFORM_MODAL_CALLBACK_ID=`eve_input_freeform_submit`,HITL_FREEFORM_MODAL_BLOCK_ID=`eve_freeform_block`,HITL_FREEFORM_MODAL_ACTION_ID=`eve_freeform_text`,HITL_ROUTE_PREFIX=`${ANSWER_ACTION_PREFIX}route:`,HITL_CODE_CHANNEL_ROUTE_PREFIX=`${ANSWER_ACTION_PREFIX}code:`,BUTTON_ACTION_ID_RE=/^(?:(?<kind>tool-approval):)?(?<requestId>.+):button:\d+$/u,TOOL_INPUT_SUFFIX="\n```",answerFromSlackAction=e=>decodeHitlResponse(e);function decodeHitlResponse(e){let t=decodeHitlActionId(e.actionId);if(t===null)return null;let n=e.selectedOptionValue??e.value;if(n===void 0||e.value!==void 0&&!t.button)return null;let r={response:parseAnswer({interactionId:t.requestId,optionId:n})};return t.kind!==void 0&&(r.kind=t.kind),t.route!==void 0&&(r.route=t.route),r}function splitEncodedRequest(e){return e.startsWith(`tool-approval:`)?{kind:`tool-approval`,requestId:e.slice(14)}:{requestId:e}}function isHitlAction(e){return e.startsWith(ANSWER_ACTION_PREFIX)}function decodeHitlActionId(e){if(!e.startsWith(`eve_input:`))return null;let t=e.slice(10),n;if(t.startsWith(`route:`)){t=t.slice(6);let e=t.indexOf(`:`),r=t.indexOf(`:`,e+1);if(e<=0||r<=e+1)return null;n={channelId:t.slice(0,e),threadTs:t.slice(e+1,r)},t=t.slice(r+1)}else if(t.startsWith(`code:`)){t=t.slice(5);let e=t.indexOf(`:`);if(e<=0)return null;n={channelId:t.slice(0,e),codeChannel:!0},t=t.slice(e+1)}let r=BUTTON_ACTION_ID_RE.exec(t),i=splitEncodedRequest(r?.groups?.requestId??t);if(!i.requestId)return null;let a=r?.groups?.kind===`tool-approval`?`tool-approval`:i.kind,o={button:r!==null,requestId:i.requestId};return a!==void 0&&(o.kind=a),n!==void 0&&(o.route=n),o}function encodeHitlActionId(e,t){let n=`${e.request.kind===`approval`?`tool-approval:`:``}${e.interactionId}`;return t===void 0?`${ANSWER_ACTION_PREFIX}${n}`:t.codeChannel===!0?`${HITL_CODE_CHANNEL_ROUTE_PREFIX}${t.channelId}:${n}`:`${HITL_ROUTE_PREFIX}${t.channelId}:${t.threadTs}:${n}`}function renderInteractionBlocks(e,t){let n={text:{text:truncateSectionText(e.request.prompt),type:`mrkdwn`},type:`section`},r=renderInteractionDetailBlocks(e),i=encodeHitlActionId(e,t),a=e.request.options,o=e.request.allowFreeform===!0||!a||a.length===0;if(a&&a.length>0&&e.request.display===`select`){let e=a.length<=6?{type:`radio_buttons`,action_id:i,options:a.map(buildOption)}:{type:`static_select`,action_id:i,options:a.map(buildOption),placeholder:{type:`plain_text`,text:`Choose an option`}};return[n,...r,{type:`actions`,elements:[e]}]}if(a&&a.length>0){let t=renderInteractionCardBlock(e,i),n=renderToolInputContainerBlock(e);return n===void 0?[t]:[t,n]}if(o){let t=i.replace(ANSWER_ACTION_PREFIX,HITL_FREEFORM_ACTION_PREFIX);return[n,...r,{type:`actions`,elements:[{type:`button`,action_id:t,text:{type:`plain_text`,text:`Type your answer`},style:`primary`,value:e.interactionId}]}]}return[n]}function renderInteractionPostParts(e,t){let n=renderInteractionBlocks(e,t),[r,...i]=n;return!isApprovalRequest(e)||r===void 0||!isBlockType(r,`card`)||i.length===0?{controls:{blocks:n,text:formatInteractionFallbackText(e)}}:{controls:{blocks:[r],text:e.request.prompt},details:{blocks:i,text:formatInteractionFallbackText(e)}}}function formatInteractionFallbackText(e){let t=formatToolInputDetails(e);return t===void 0?e.request.prompt:`${e.request.prompt}\n${t}`}function buildFreeformModalView(e){let t=e.prompt?truncateModalTitle(e.prompt):`Your answer`,n=e.prompt?[{type:`section`,text:{type:`mrkdwn`,text:truncateSectionText(e.prompt)}}]:[];return{type:`modal`,callback_id:HITL_FREEFORM_MODAL_CALLBACK_ID,private_metadata:JSON.stringify(e.metadata),title:{type:`plain_text`,text:t},submit:{type:`plain_text`,text:`Submit`},close:{type:`plain_text`,text:`Cancel`},blocks:[...n,{type:`input`,block_id:HITL_FREEFORM_MODAL_BLOCK_ID,element:{type:`plain_text_input`,action_id:HITL_FREEFORM_MODAL_ACTION_ID,multiline:!0,placeholder:{type:`plain_text`,text:`Type your answer here...`}},label:{type:`plain_text`,text:`Answer`}}]}}function isFreeformAction(e){return e.startsWith(HITL_FREEFORM_ACTION_PREFIX)}function decodeFreeformHitlActionId(e){return isFreeformAction(e)?decodeHitlActionId(`${ANSWER_ACTION_PREFIX}${e.slice(19)}`):null}function buildCardButton(e,t,n){let r={type:`button`,text:{type:`plain_text`,text:truncatePlainText(e.label),emoji:!1},action_id:`${t}:button:${n}`,value:e.id};return(e.style===`primary`||e.style===`danger`)&&(r.style=e.style),r}function buildOption(e){let t={text:{text:truncatePlainText(e.label),type:`plain_text`},value:e.id},n=truncatePlainText(e.description);return n&&n.length>0&&(t.description={text:n,type:`plain_text`}),t}function renderInteractionCardBlock(e,t){let n={type:`card`,body:{type:`mrkdwn`,text:truncateCardBodyText(`*${e.request.prompt}*`),verbatim:!1},actions:cardButtonOptions(e).map((e,n)=>buildCardButton(e,t,n))};return isApprovalRequest(e)&&(n.subtext={type:`mrkdwn`,text:`I've paused until someone approves or denies.`}),n}function cardButtonOptions(e){let t=e.request.options??[];if(!isApprovalRequest(e))return t.map(toCardButtonOption);let n=t.find(e=>e.id===`approve`),r=t.find(e=>e.id===`deny`);return!n||!r?t.map(toCardButtonOption):[{id:r.id,label:`Deny`},{id:n.id,label:`Approve`,style:`primary`}]}function toCardButtonOption(e){let t={id:e.id,label:e.label};return e.style===`primary`||e.style===`danger`?{...t,style:e.style}:t}function buildAnsweredBlocks(e){let t=[];for(let n of e.promptBlocks)n!=null&&t.push(n);let n=truncate(e.answerLabel,SLACK_SECTION_TEXT_MAX_LENGTH-20-1);return t.push({type:`section`,text:{type:`mrkdwn`,text:`:white_check_mark: *${n}*`}}),e.userId&&e.userId.length>0&&t.push({type:`context`,elements:[{type:`mrkdwn`,text:`Answered by <@${e.userId}>`}]}),t}function renderInteractionDetailBlocks(e){let t=formatToolInputDetails(e);return t===void 0?[]:[{type:`section`,text:{type:`mrkdwn`,text:t}}]}function renderToolInputContainerBlock(e){let t=formatToolInputContainerText(e);if(t!==void 0)return{type:`container`,title:{type:`plain_text`,text:`Tool input`},is_collapsible:!0,default_collapsed:!0,child_blocks:[{type:`section`,text:{type:`mrkdwn`,text:t}}]}}function formatToolInputContainerText(e){if(!isApprovalRequest(e))return;let t=JSON.stringify(e.call?.input??{},null,2);if(t===`{}`)return;let n=SLACK_SECTION_TEXT_MAX_LENGTH-4-4;return`\`\`\`
|
|
2
2
|
${truncate(t,n)}${TOOL_INPUT_SUFFIX}`}function formatToolInputDetails(e){if(!isApprovalRequest(e))return;let t=JSON.stringify(e.call?.input??{},null,2);if(t===`{}`)return;let n=SLACK_SECTION_TEXT_MAX_LENGTH-17-4;return`*Tool input*
|
|
3
3
|
\`\`\`
|
|
4
4
|
${truncate(t,n)}${TOOL_INPUT_SUFFIX}`}function isBlockType(e,t){return typeof e==`object`&&!!e&&e.type===t}function isApprovalRequest(e){return e.request.kind===`approval`}export{ANSWER_ACTION_PREFIX,HITL_FREEFORM_ACTION_PREFIX,HITL_FREEFORM_MODAL_ACTION_ID,HITL_FREEFORM_MODAL_BLOCK_ID,HITL_FREEFORM_MODAL_CALLBACK_ID,answerFromSlackAction,buildAnsweredBlocks,buildFreeformModalView,decodeFreeformHitlActionId,decodeHitlActionId,decodeHitlResponse,formatInteractionFallbackText,isFreeformAction,isHitlAction,renderInteractionBlocks,renderInteractionPostParts};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { openCodeChannel, type SlackCodeChannelOpenResult, } from "#public/channels/slack/open-code-channel.js";
|
|
2
|
+
export { slackChannel, type SlackApiResponse, type SlackApprovalChannel, type SlackAuthorizationEventContext, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelContext, type SlackChannelCredentials, type SlackChannelState, type SlackCodeChannelTarget, type SlackContext, type SlackEventContext, type SlackEventRespondFn, type SlackEventRespondOptions, type SlackEventSendFn, type SlackEventSendOptions, type SlackHandle, type SlackInboundEventContext, type SlackInboundMessageContext, type SlackOpenCodeChannelOptions, type SlackOpenedCodeChannel, type SlackInboundResult, type SlackInitialMessage, type SlackAnswerContext, type SlackAnswerResult, type SlackAnswerSubmission, type SlackInstrumentationMetadata, type SlackInteractionAction, type SlackInteractionContext, type SlackSlashCommand, type SlackSlashCommandContext, type SlackShortcut, type SlackShortcutContext, type SlackShortcutMessage, type SlackMentionResult, type SlackReceiveTarget, type SlackSessionTarget, type SlackRespondOptions, type SlackSendOptions, type SlackSessionOperations, type SlackThread, type SlackWebhookVerifier, type SlackWorkspaceHandle, } from "#public/channels/slack/slackChannel.js";
|
|
2
3
|
export type { SlackAttachment, SlackAuthor, SlackEvent, SlackEventEnvelope, SlackInboundContext, SlackMessage, } from "#public/channels/slack/inbound.js";
|
|
3
|
-
export { callSlackApi, resolveSlackBotToken, slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackBotTokenContext, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
|
|
4
|
+
export { callSlackApi, resolveSlackBotToken, slackCodeChannelContinuationToken, slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackBotTokenContext, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
|
|
4
5
|
export { type SlackFetch, type SlackTransportOptions } from "#public/channels/slack/transport.js";
|
|
5
6
|
export { defaultSlackAuth } from "#public/channels/slack/defaults.js";
|
|
6
7
|
export { postCompletedSlackReply, type SlackCompletedReply, } from "#public/channels/slack/reply-delivery.js";
|
|
7
|
-
export { defineSlackRenderer, type SlackRenderer, type SlackRendererEvents, type SlackRenderHandler, type SlackRenderNext,
|
|
8
|
+
export { defineSlackRenderer, type SlackCardMessage, type SlackInteractionCard, type SlackInteractionCardParts, type SlackRenderer, type SlackRendererEvents, type SlackRenderHandler, type SlackRenderNext, } from "#public/channels/slack/renderers.js";
|
|
8
9
|
export type { TaskCard } from "#channel/task-card.js";
|
|
9
10
|
export { loadThreadContextMessages, type LoadThreadContextMessagesOptions, type ThreadContextSince, } from "#public/channels/slack/thread.js";
|
|
10
11
|
export { cardToBlocks, cardToFallbackText, type BlockKitBlock, } from "#public/channels/slack/blocks.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{callSlackApi,resolveSlackBotToken,slackContinuationToken}from"#public/channels/slack/api.js";import{Actions,Button,Card,CardLink,CardText,Divider,ExternalSelect,Field,Fields,Image,LinkButton,Modal,RadioSelect,Section,Select,SelectOption,Table,TextInput,cardChildToFallbackText,isCardElement}from"#public/channels/slack/cards.js";import{cardToBlocks,cardToFallbackText}from"#public/channels/slack/blocks.js";import{
|
|
1
|
+
import{callSlackApi,resolveSlackBotToken,slackCodeChannelContinuationToken,slackContinuationToken}from"#public/channels/slack/api.js";import{Actions,Button,Card,CardLink,CardText,Divider,ExternalSelect,Field,Fields,Image,LinkButton,Modal,RadioSelect,Section,Select,SelectOption,Table,TextInput,cardChildToFallbackText,isCardElement}from"#public/channels/slack/cards.js";import{cardToBlocks,cardToFallbackText}from"#public/channels/slack/blocks.js";import{postCompletedSlackReply}from"#public/channels/slack/reply-delivery.js";import{openCodeChannel}from"#public/channels/slack/open-code-channel.js";import{slackChannel}from"#public/channels/slack/slackChannel.js";import{defaultSlackAuth}from"#public/channels/slack/defaults.js";import{defineSlackRenderer}from"#public/channels/slack/renderers.js";import{loadThreadContextMessages}from"#public/channels/slack/thread.js";import{ANSWER_ACTION_PREFIX,answerFromSlackAction,renderInteractionBlocks}from"#public/channels/slack/hitl.js";export{ANSWER_ACTION_PREFIX,Actions,Button,Card,CardLink,CardText,Divider,ExternalSelect,Field,Fields,Image,LinkButton,Modal,RadioSelect,Section,Select,SelectOption,Table,TextInput,answerFromSlackAction,callSlackApi,cardChildToFallbackText,cardToBlocks,cardToFallbackText,defaultSlackAuth,defineSlackRenderer,isCardElement,loadThreadContextMessages,openCodeChannel,postCompletedSlackReply,renderInteractionBlocks,resolveSlackBotToken,slackChannel,slackCodeChannelContinuationToken,slackContinuationToken};
|
|
@@ -15,8 +15,6 @@ export declare function answeredBlocksFromActionBlock(input: {
|
|
|
15
15
|
readonly block: unknown;
|
|
16
16
|
readonly userId?: string;
|
|
17
17
|
}): unknown[] | undefined;
|
|
18
|
-
/** Settles a pressed HITL card as answered by the presser with the choice they pressed. */
|
|
19
|
-
export declare function updateAnsweredHitlCard(interaction: ParsedBlockActionsPayload, deps: InteractionHandlerDeps): Promise<void>;
|
|
20
18
|
/** Replaces a pressed HITL card's controls with `card`. */
|
|
21
19
|
export declare function settleHitlCard(interaction: ParsedBlockActionsPayload, deps: InteractionHandlerDeps, card: SettledCard): Promise<void>;
|
|
22
20
|
/** Replaces a freeform card's controls with `card`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{truncate}from"#public/channels/text.js";import{callSlackApi}from"#public/channels/slack/api.js";import{SLACK_CARD_SUBTEXT_MAX_LENGTH,truncateCardSubtext}from"#public/channels/slack/limits.js";import{buildAnsweredBlocks,isHitlAction}from"#public/channels/slack/hitl.js";function buildSettledHitlMessageBlocks(e){let t=e.messageBlocks.findIndex(t=>blockContainsActionId(t,e.actionId));if(t===-1)return buildAnsweredBlocks({promptBlocks:findPromptBlocks(e.messageBlocks),answerLabel:e.card.label,userId:e.card.userId});let n=e.messageBlocks[t],r=answeredBlocksFromActionBlock({answerLabel:e.card.label,block:n,userId:e.card.userId})??buildAnsweredBlocks({promptBlocks:promptBlocksFromActionBlock(n),answerLabel:e.card.label,userId:e.card.userId});return[...e.messageBlocks.slice(0,t),...r,...e.messageBlocks.slice(t+1)]}function findPromptBlocks(e){let t=[];for(let n of e){if(!isObjectRecord(n))continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function blockContainsActionId(e,t){return isObjectRecord(e)?actionsContainActionId(e.elements,t)||actionsContainActionId(e.actions,t):!1}function actionsContainActionId(e,t){return Array.isArray(e)?e.some(e=>isObjectRecord(e)&&e.action_id===t):!1}function answeredBlocksFromActionBlock(e){if(!isObjectRecord(e.block)||e.block.type!==`card`)return;let{actions:t,subtext:n,...r}=e.block,i={...r,subtext:{type:`mrkdwn`,text:formatAnsweredCardSubtext(e),verbatim:!1}};return hasCardContent(i)?[i]:void 0}function formatAnsweredCardSubtext(e){let t=e.userId===void 0?``:` by <@${e.userId}>`,n=SLACK_CARD_SUBTEXT_MAX_LENGTH-20-1-t.length,r=truncate(e.answerLabel,n);return truncateCardSubtext(`:white_check_mark: *${r}*${t}`)}function promptBlocksFromActionBlock(e){if(!isObjectRecord(e)||e.type!==`card`)return[];let{actions:t,...n}=e;return hasCardContent(n)?[n]:[]}function hasCardContent(e){return e.body!==void 0||e.title!==void 0||e.hero_image!==void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}async function
|
|
1
|
+
import{truncate}from"#public/channels/text.js";import{callSlackApi}from"#public/channels/slack/api.js";import{SLACK_CARD_SUBTEXT_MAX_LENGTH,truncateCardSubtext}from"#public/channels/slack/limits.js";import{buildAnsweredBlocks,isHitlAction}from"#public/channels/slack/hitl.js";function buildSettledHitlMessageBlocks(e){let t=e.messageBlocks.findIndex(t=>blockContainsActionId(t,e.actionId));if(t===-1)return buildAnsweredBlocks({promptBlocks:findPromptBlocks(e.messageBlocks),answerLabel:e.card.label,userId:e.card.userId});let n=e.messageBlocks[t],r=answeredBlocksFromActionBlock({answerLabel:e.card.label,block:n,userId:e.card.userId})??buildAnsweredBlocks({promptBlocks:promptBlocksFromActionBlock(n),answerLabel:e.card.label,userId:e.card.userId});return[...e.messageBlocks.slice(0,t),...r,...e.messageBlocks.slice(t+1)]}function findPromptBlocks(e){let t=[];for(let n of e){if(!isObjectRecord(n))continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function blockContainsActionId(e,t){return isObjectRecord(e)?actionsContainActionId(e.elements,t)||actionsContainActionId(e.actions,t):!1}function actionsContainActionId(e,t){return Array.isArray(e)?e.some(e=>isObjectRecord(e)&&e.action_id===t):!1}function answeredBlocksFromActionBlock(e){if(!isObjectRecord(e.block)||e.block.type!==`card`)return;let{actions:t,subtext:n,...r}=e.block,i={...r,subtext:{type:`mrkdwn`,text:formatAnsweredCardSubtext(e),verbatim:!1}};return hasCardContent(i)?[i]:void 0}function formatAnsweredCardSubtext(e){let t=e.userId===void 0?``:` by <@${e.userId}>`,n=SLACK_CARD_SUBTEXT_MAX_LENGTH-20-1-t.length,r=truncate(e.answerLabel,n);return truncateCardSubtext(`:white_check_mark: *${r}*${t}`)}function promptBlocksFromActionBlock(e){if(!isObjectRecord(e)||e.type!==`card`)return[];let{actions:t,...n}=e;return hasCardContent(n)?[n]:[]}function hasCardContent(e){return e.body!==void 0||e.title!==void 0||e.hero_image!==void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}async function settleHitlCard(e,t,n){let r=e.actions.find(e=>isHitlAction(e.actionId));r?.messageTs&&await updateSettledCard({blocks:buildSettledHitlMessageBlocks({actionId:r.actionId,card:n,messageBlocks:e.messageBlocks}),card:n,channelId:e.channelId,deps:t,installationTeamId:e.installationTeamId,messageTs:r.messageTs})}async function settleFreeformCard(e){await updateSettledCard({...e,blocks:buildAnsweredBlocks({promptBlocks:[],answerLabel:e.card.label,userId:e.card.userId})})}async function updateSettledCard(e){await callSlackApi({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,context:{teamId:e.installationTeamId},operation:`chat.update`,body:{channel:e.channelId,ts:e.messageTs,blocks:e.blocks,text:e.card.text}})}export{answeredBlocksFromActionBlock,settleFreeformCard,settleHitlCard};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"#internal/logging.js";import{parseAnswer}from"#shared/input.js";import{INACTIVE_REQUEST_NOTICE,respondOrRetireInactivePrompt}from"#channel/inactive-prompt.js";import{buildSlackBinding,buildSlackWorkspaceHandle,callSlackApi,resolveSlackBotToken,slackContinuationToken}from"#public/channels/slack/api.js";import{buildSlackAuthContext}from"#public/channels/slack/auth.js";import{readSlackTextObject}from"#public/channels/slack/inbound-content.js";import{isSlackResponseError}from"#public/channels/slack/transport.js";import{HITL_FREEFORM_MODAL_ACTION_ID,HITL_FREEFORM_MODAL_BLOCK_ID,HITL_FREEFORM_MODAL_CALLBACK_ID,buildFreeformModalView,decodeFreeformHitlActionId,decodeHitlResponse,isFreeformAction,isHitlAction}from"#public/channels/slack/hitl.js";import{settleFreeformCard,settleHitlCard,updateAnsweredHitlCard}from"#public/channels/slack/interaction-cards.js";import{dispatchSignInCancel}from"#public/channels/slack/sign-in-cancel.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{authorizeAnswer}from"#public/channels/slack/answers.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{dispatchSlashCommand}from"#public/channels/slack/slash-command.js";const log=createLogger(`slack.interactions`);function parseBlockActionsPayload(e){if(isSharedBlockActionsPayload(e))return parseSharedBlockActionsPayload(e);let t=e,n=t.actions;if(!Array.isArray(n))return null;let r=t.channel?.id,i=t.message,a=i?.thread_ts??i?.ts;if(!r||!a)return null;let o=t.team,s=t.user,c=s.team_id??o?.id,l={id:s.id,username:s.username,name:s.name},u=i?.blocks??[];return{actions:n.map(e=>({actionId:String(e.action_id??``),value:e.value==null?void 0:String(e.value),blockId:e.block_id==null?void 0:String(e.block_id),selectedOptionValue:extractSelectedOptionValue(e),messageTs:i?.ts,triggerId:typeof t.trigger_id==`string`?t.trigger_id:void 0,label:extractActionLabel(e),user:l})),channelId:r,installationTeamId:readInstallationTeamId(t),threadTs:a,teamId:c,messageBlocks:u}}function isSharedBlockActionsPayload(e){return e.kind===`block_actions`&&Array.isArray(e.actions)}function parseSharedBlockActionsPayload(e){return!e.channelId||!e.threadTs?null:{actions:e.actions.map(t=>({actionId:t.actionId,value:t.value,blockId:t.blockId,selectedOptionValue:t.selectedOptionValue,messageTs:e.messageTs,triggerId:e.triggerId,label:t.label,user:{id:t.user?.id??e.userId,username:t.user?.username,name:t.user?.name}})),channelId:e.channelId,installationTeamId:readInstallationTeamId(e.raw),threadTs:e.threadTs,teamId:e.user?.teamId??e.teamId,messageBlocks:e.messageBlocks??[]}}function extractSelectedOptionValue(e){let t=e.selected_option;return typeof t?.value==`string`?t.value:void 0}function extractActionLabel(e){let t=e.selected_option?.text?.text;if(typeof t==`string`&&t.length>0)return t;let n=e.text?.text;if(typeof n==`string`&&n.length>0)return n}function findPromptBlocks(e){let t=[];for(let n of e){if(typeof n!=`object`||!n)continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function readPromptTextFromBlocks(e){let t=findPromptBlocks(e)[0],n=readSlackTextObject(t?.text);return n.length>0?n:void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}function readInstallationTeamId(e){if(!isObjectRecord(e))return;let t=isObjectRecord(e.view)?e.view:void 0,n=isObjectRecord(e.team)?e.team:void 0,r=isObjectRecord(e.user)?e.user:void 0;return[t?.app_installed_team_id,e.app_installed_team_id,n?.id,r?.team_id,t?.team_id].find(e=>typeof e==`string`&&e.length>0)}async function handleInteractionPost(e,t,n){let r=new Response(`ok`,{status:200}),i;try{i=parseSlackWebhookBody(e,{contentType:`application/x-www-form-urlencoded`})}catch{return log.warn(`failed to parse Slack interaction payload`),r}if(i.kind===`view_submission`)return handleViewSubmission(i,t,n);if(i.kind===`slash_command`)return dispatchSlashCommand(i,t,n),new Response(null,{status:200});if(i.kind===`unsupported`){let e=parseShortcutPayload(i.raw);return e===null?(log.warn(`unsupported Slack interaction payload ignored`,{type:i.type}),r):(dispatchShortcut(e,readInstallationTeamId(i.raw),t,n),new Response(null,{status:200}))}if(i.kind!==`block_actions`)return log.warn(`unsupported Slack interaction payload ignored`,{type:i.kind}),r;if(dispatchSignInCancel(i.raw,t))return r;let a=parseBlockActionsPayload(i);if(!a)return r;let o=a.actions.find(e=>isFreeformAction(e.actionId));if(o)return await openFreeformModal({payload:i.raw,interaction:a,freeformAction:o,deps:n}),r;let s=a.actions.flatMap(e=>{let t=decodeHitlResponse(e);return t===null?[]:[{action:e,derived:t}]});if(s.length>0){let e=s[0].action.user,r=s[0].derived.route;t.waitUntil(dispatchBlockAnswers({ctx:t,deps:n,interaction:a,route:r,submission:{type:`block_actions`,actions:s.map(({action:e})=>e),answers:s.map(({derived:e})=>e.response),messageTs:s[0].action.messageTs,user:e}}))}let c=n.config.onInteraction;if(c){let e=a.actions.filter(e=>!isHitlAction(e.actionId));if(e.length>0){let r=e[0].user,{thread:i,slack:o}=buildSlackBinding({api:n.api,botToken:n.config.credentials?.botToken,channelId:a.channelId,threadTs:a.threadTs,installationTeamId:a.installationTeamId,teamId:a.teamId}),s={...bindSlackSessionOperations({address:slackContinuationToken({channelId:a.channelId,threadTs:a.threadTs}),defaultAuth:buildSlackAuthContext({channelId:a.channelId,installationTeamId:a.installationTeamId,teamId:a.teamId,threadTs:a.threadTs,userId:r.id,userName:r.username??r.name}),from:t.from,resolveSession:t.resolveSession,state:{channelId:a.channelId,installationTeamId:a.installationTeamId??null,teamId:a.teamId??null,threadTs:a.threadTs,triggeringUserId:r.id}}),thread:i,slack:o};for(let n of e)t.waitUntil(Promise.resolve(c(n,s)).catch(e=>{log.error(`custom interaction handler failed`,{error:e})}))}}return r}function parseShortcutPayload(e){if(!isObjectRecord(e))return null;let t=e.type;if(t!==`message_action`&&t!==`shortcut`)return null;let n=readRequiredString(e.callback_id),r=readRequiredString(e.trigger_id),i=isObjectRecord(e.user)?e.user:void 0,a=readRequiredString(i?.id);if(n===null||r===null||a===null)return null;let o=isObjectRecord(e.team)?e.team:void 0,s=readOptionalString(i?.team_id)??readOptionalString(o?.id),c={id:a,username:readOptionalString(i?.username),name:readOptionalString(i?.name)};if(t===`shortcut`)return{type:t,callbackId:n,triggerId:r,user:c,teamId:s};let l=isObjectRecord(e.channel)?e.channel:void 0,u=isObjectRecord(e.message)?e.message:void 0,d=readRequiredString(l?.id),f=readRequiredString(u?.ts);return d===null||f===null||u===void 0?null:{type:t,callbackId:n,triggerId:r,user:c,teamId:s,channelId:d,message:{text:typeof u.text==`string`?u.text:``,ts:f,threadTs:readOptionalString(u.thread_ts),userId:readOptionalString(u.user)},responseUrl:readOptionalString(e.response_url)}}function readRequiredString(e){return typeof e==`string`&&e.length>0?e:null}function readOptionalString(e){return typeof e==`string`&&e.length>0?e:void 0}function dispatchShortcut(e,t,n,r){let i=r.config.onShortcut;if(i===void 0){log.warn(`Slack shortcut ignored because onShortcut is not configured`,{type:e.type});return}let a={slack:buildSlackWorkspaceHandle({api:r.api,botToken:r.config.credentials?.botToken,installationTeamId:t,teamId:e.teamId})};dispatchInteractionHook(()=>i(e,a),n,`shortcut handler failed`)}function dispatchInteractionHook(e,t,n){t.waitUntil(Promise.resolve().then(e).catch(e=>{log.error(n,{error:e})}))}async function dispatchBlockAnswers(e){let t=e.route?.channelId??e.interaction.channelId,n=e.route?.threadTs??e.interaction.threadTs,r=await authorizeAnswer({channelId:t,deps:e.deps,installationTeamId:e.interaction.installationTeamId,submission:e.submission,teamId:e.interaction.teamId,threadTs:n});if(r===null)return;let i=await respondOrRetireInactivePrompt(()=>e.ctx.from(slackContinuationToken({channelId:t,threadTs:n})).respond(e.submission.answers,{auth:r.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleHitlCard(e.interaction,e.deps,{label:t,text:t})},log);if(i.outcome===`inactive`&&await Promise.all([i.retirement,postEphemeralNotice({channelId:e.interaction.channelId,deps:e.deps,installationTeamId:e.interaction.installationTeamId,teamId:e.interaction.teamId,threadTs:e.interaction.threadTs,userId:e.submission.user.id})]),i.outcome===`delivered`&&!e.submission.actions.some(e=>decodeHitlResponse(e)?.kind===`tool-approval`))try{await updateAnsweredHitlCard(e.interaction,e.deps)}catch(e){log.error(`HITL answered-card update failed`,{error:e})}}async function postEphemeralNotice(e){let{thread:t}=buildSlackBinding({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,channelId:e.channelId,installationTeamId:e.installationTeamId,teamId:e.teamId,threadTs:e.threadTs});try{await t.postEphemeral(e.userId,INACTIVE_REQUEST_NOTICE)}catch(e){log.error(`inactive prompt notice failed`,{error:e})}}async function openFreeformModal(e){let t=e.payload.trigger_id;if(typeof t!=`string`||t.length===0){log.warn(`freeform button click missing trigger_id — cannot open modal`);return}let n=decodeFreeformHitlActionId(e.freeformAction.actionId);if(n===null){log.warn(`freeform button click carries invalid metadata`);return}let r=n.requestId;if(!r){log.warn(`freeform button click missing requestId`);return}let i=e.freeformAction.messageTs;if(!i){log.warn(`freeform button click missing messageTs`);return}let a=n?.route?.channelId??e.interaction.channelId,o=n?.route?.threadTs??e.interaction.threadTs,s={continuationToken:slackContinuationToken({channelId:a,threadTs:o}),channelId:a,installationTeamId:e.interaction.installationTeamId,messageChannelId:e.interaction.channelId,threadTs:o,messageTs:i,requestId:r},c=readPromptTextFromBlocks(e.interaction.messageBlocks),l=buildFreeformModalView({metadata:s,prompt:c}),u=await resolveSlackBotToken(e.deps.config.credentials?.botToken,{teamId:e.interaction.installationTeamId});try{await callSlackApi({api:e.deps.api,botToken:u,operation:`views.open`,body:{trigger_id:t,view:l}})}catch(e){if(!isSlackResponseError(e))throw e;log.error(`Slack views.open failed`,{error:e})}}async function handleViewSubmission(e,t,n){let r=new Response(null,{status:200});if(e.callbackId!==HITL_FREEFORM_MODAL_CALLBACK_ID)return r;let i;try{i=JSON.parse(e.privateMetadata??``)}catch{return log.warn(`freeform view_submission carries invalid private_metadata`),r}if(!i.continuationToken||!i.requestId||!i.messageTs||!i.channelId||!i.threadTs)return r;let a=e.values?.find(e=>e.blockId===HITL_FREEFORM_MODAL_BLOCK_ID&&e.actionId===HITL_FREEFORM_MODAL_ACTION_ID)?.value??``;if(a.length===0)return r;let o=e.user,s=e.userId,c=o?.teamId??e.teamId??null,l=i.installationTeamId??readInstallationTeamId(e.raw),u={type:`view_submission`,answers:[parseAnswer({interactionId:i.requestId,text:a})],messageTs:i.messageTs,user:{id:s,username:o?.username,name:o?.name}};return t.waitUntil(dispatchViewAnswer({ctx:t,deps:n,installationTeamId:l,metadata:i,submission:u,teamId:c,text:a})),r}async function dispatchViewAnswer(e){let t=await authorizeAnswer({channelId:e.metadata.channelId,deps:e.deps,installationTeamId:e.installationTeamId,submission:e.submission,teamId:e.teamId,threadTs:e.metadata.threadTs});if(t===null)return;let n=e.metadata.messageChannelId??e.metadata.channelId,r=await respondOrRetireInactivePrompt(()=>e.ctx.from(e.metadata.continuationToken).respond(e.submission.answers,{auth:t.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleFreeformCard({card:{label:t,text:t},channelId:n,deps:e.deps,installationTeamId:e.installationTeamId,messageTs:e.metadata.messageTs})},log);if(r.outcome===`inactive`&&await Promise.all([r.retirement,postEphemeralNotice({channelId:n,deps:e.deps,installationTeamId:e.installationTeamId,teamId:e.teamId??void 0,threadTs:n===e.metadata.channelId?e.metadata.threadTs:``,userId:e.submission.user.id})]),r.outcome===`delivered`)try{await settleFreeformCard({card:{label:e.text,text:`Answered: ${e.text}`,userId:e.submission.user.id},channelId:n,deps:e.deps,installationTeamId:e.installationTeamId,messageTs:e.metadata.messageTs})}catch(e){log.error(`freeform answered-card update failed`,{error:e})}}export{handleInteractionPost,parseBlockActionsPayload,parseShortcutPayload};
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{parseAnswer}from"#shared/input.js";import{INACTIVE_REQUEST_NOTICE,respondOrRetireInactivePrompt}from"#channel/inactive-prompt.js";import{buildSlackBinding,buildSlackWorkspaceHandle,callSlackApi,resolveSlackBotToken,slackCodeChannelContinuationToken,slackContinuationToken,slackSessionToken}from"#public/channels/slack/api.js";import{buildSlackAuthContext}from"#public/channels/slack/auth.js";import{readSlackTextObject}from"#public/channels/slack/inbound-content.js";import{isSlackCodeChannel}from"#public/channels/slack/code-channel.js";import{isSlackResponseError}from"#public/channels/slack/transport.js";import{dispatchSignInCancel}from"#public/channels/slack/sign-in-cancel.js";import{HITL_FREEFORM_MODAL_ACTION_ID,HITL_FREEFORM_MODAL_BLOCK_ID,HITL_FREEFORM_MODAL_CALLBACK_ID,buildFreeformModalView,decodeFreeformHitlActionId,decodeHitlResponse,isFreeformAction,isHitlAction}from"#public/channels/slack/hitl.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{settleFreeformCard,settleHitlCard}from"#public/channels/slack/interaction-cards.js";import{authorizeAnswer}from"#public/channels/slack/answers.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{dispatchSlashCommand}from"#public/channels/slack/slash-command.js";const log=createLogger(`slack.interactions`);function parseBlockActionsPayload(e){if(isSharedBlockActionsPayload(e))return parseSharedBlockActionsPayload(e);let t=e,n=t.actions;if(!Array.isArray(n))return null;let r=t.channel?.id,i=t.message,a=i?.thread_ts??i?.ts;if(!r||!a)return null;let o=t.team,s=t.user,c=s.team_id??o?.id,l={id:s.id,username:s.username,name:s.name},u=i?.blocks??[];return{actions:n.map(e=>({actionId:String(e.action_id??``),value:e.value==null?void 0:String(e.value),blockId:e.block_id==null?void 0:String(e.block_id),selectedOptionValue:extractSelectedOptionValue(e),messageTs:i?.ts,triggerId:typeof t.trigger_id==`string`?t.trigger_id:void 0,label:extractActionLabel(e),user:l})),channelId:r,installationTeamId:readInstallationTeamId(t),threadTs:a,teamId:c,messageBlocks:u}}function isSharedBlockActionsPayload(e){return e.kind===`block_actions`&&Array.isArray(e.actions)}function parseSharedBlockActionsPayload(e){return!e.channelId||!e.threadTs?null:{actions:e.actions.map(t=>({actionId:t.actionId,value:t.value,blockId:t.blockId,selectedOptionValue:t.selectedOptionValue,messageTs:e.messageTs,triggerId:e.triggerId,label:t.label,user:{id:t.user?.id??e.userId,username:t.user?.username,name:t.user?.name}})),channelId:e.channelId,installationTeamId:readInstallationTeamId(e.raw),threadTs:e.threadTs,teamId:e.user?.teamId??e.teamId,messageBlocks:e.messageBlocks??[]}}function extractSelectedOptionValue(e){let t=e.selected_option;return typeof t?.value==`string`?t.value:void 0}function extractActionLabel(e){let t=e.selected_option?.text?.text;if(typeof t==`string`&&t.length>0)return t;let n=e.text?.text;if(typeof n==`string`&&n.length>0)return n}function findPromptBlocks(e){let t=[];for(let n of e){if(typeof n!=`object`||!n)continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function readPromptTextFromBlocks(e){let t=findPromptBlocks(e)[0],n=readSlackTextObject(t?.text);return n.length>0?n:void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}function readInstallationTeamId(e){if(!isObjectRecord(e))return;let t=isObjectRecord(e.view)?e.view:void 0,n=isObjectRecord(e.team)?e.team:void 0,r=isObjectRecord(e.user)?e.user:void 0;return[t?.app_installed_team_id,e.app_installed_team_id,n?.id,r?.team_id,t?.team_id].find(e=>typeof e==`string`&&e.length>0)}async function handleInteractionPost(e,t,n){let r=new Response(`ok`,{status:200}),i;try{i=parseSlackWebhookBody(e,{contentType:`application/x-www-form-urlencoded`})}catch{return log.warn(`failed to parse Slack interaction payload`),r}if(i.kind===`view_submission`)return handleViewSubmission(i,t,n);if(i.kind===`slash_command`)return dispatchSlashCommand(i,t,n),new Response(null,{status:200});if(i.kind===`unsupported`){let e=parseShortcutPayload(i.raw);return e===null?(log.warn(`unsupported Slack interaction payload ignored`,{type:i.type}),r):(dispatchShortcut(e,readInstallationTeamId(i.raw),t,n),new Response(null,{status:200}))}if(i.kind!==`block_actions`)return log.warn(`unsupported Slack interaction payload ignored`,{type:i.kind}),r;if(dispatchSignInCancel(i.raw,t))return r;let a=parseBlockActionsPayload(i);if(!a)return r;let o=a.actions.find(e=>isFreeformAction(e.actionId));if(o)return await openFreeformModal({payload:i.raw,interaction:a,freeformAction:o,deps:n}),r;let s=a.actions.flatMap(e=>{let t=decodeHitlResponse(e);return t===null?[]:[{action:e,derived:t}]});if(s.length>0){let e=s[0].action.user,r=s[0].derived.route;t.waitUntil(dispatchBlockAnswers({ctx:t,deps:n,interaction:a,route:r,submission:{type:`block_actions`,actions:s.map(({action:e})=>e),answers:s.map(({derived:e})=>e.response),messageTs:s[0].action.messageTs,user:e}}))}let c=n.config.onInteraction;if(c){let e=a.actions.filter(e=>!isHitlAction(e.actionId));e.length>0&&t.waitUntil(dispatchCustomInteractions({ctx:t,customActions:e,deps:n,interaction:a,onInteraction:c}).catch(e=>{log.error(`custom interaction dispatch failed`,{error:e})}))}return r}async function dispatchCustomInteractions(e){let{ctx:t,customActions:n,deps:r,interaction:i,onInteraction:a}=e,o=n[0].user,s=r.config.codeChannels===!0&&await isSlackCodeChannel(i.channelId,buildSlackWorkspaceHandle({api:r.api,botToken:r.config.credentials?.botToken,installationTeamId:i.installationTeamId,teamId:i.teamId}).request),c=s?``:i.threadTs,l=s&&i.threadTs!==n[0].messageTs?i.threadTs:void 0,{thread:u,slack:d}=buildSlackBinding({api:r.api,botToken:r.config.credentials?.botToken,channelId:i.channelId,threadTs:c,codeChannel:s,replyThreadTs:()=>l,installationTeamId:i.installationTeamId,teamId:i.teamId}),f={...bindSlackSessionOperations({address:s?slackCodeChannelContinuationToken(i.channelId):slackContinuationToken({channelId:i.channelId,threadTs:c}),defaultAuth:buildSlackAuthContext({channelId:i.channelId,installationTeamId:i.installationTeamId,teamId:i.teamId,threadTs:c,userId:o.id,userName:o.username??o.name}),from:t.from,resolveSession:t.resolveSession,state:{channelId:i.channelId,installationTeamId:i.installationTeamId??null,teamId:i.teamId??null,threadTs:c||null,triggeringUserId:o.id,...s&&{codeChannel:!0,replyThreadTs:l??null}}}),thread:u,slack:d};await Promise.all(n.map(e=>Promise.resolve().then(()=>a(e,f)).catch(e=>{log.error(`custom interaction handler failed`,{error:e})})))}function parseShortcutPayload(e){if(!isObjectRecord(e))return null;let t=e.type;if(t!==`message_action`&&t!==`shortcut`)return null;let n=readRequiredString(e.callback_id),r=readRequiredString(e.trigger_id),i=isObjectRecord(e.user)?e.user:void 0,a=readRequiredString(i?.id);if(n===null||r===null||a===null)return null;let o=isObjectRecord(e.team)?e.team:void 0,s=readOptionalString(i?.team_id)??readOptionalString(o?.id),c={id:a,username:readOptionalString(i?.username),name:readOptionalString(i?.name)};if(t===`shortcut`)return{type:t,callbackId:n,triggerId:r,user:c,teamId:s};let l=isObjectRecord(e.channel)?e.channel:void 0,u=isObjectRecord(e.message)?e.message:void 0,d=readRequiredString(l?.id),f=readRequiredString(u?.ts);return d===null||f===null||u===void 0?null:{type:t,callbackId:n,triggerId:r,user:c,teamId:s,channelId:d,message:{text:typeof u.text==`string`?u.text:``,ts:f,threadTs:readOptionalString(u.thread_ts),userId:readOptionalString(u.user)},responseUrl:readOptionalString(e.response_url)}}function readRequiredString(e){return typeof e==`string`&&e.length>0?e:null}function readOptionalString(e){return typeof e==`string`&&e.length>0?e:void 0}function dispatchShortcut(e,t,n,r){let i=r.config.onShortcut;if(i===void 0){log.warn(`Slack shortcut ignored because onShortcut is not configured`,{type:e.type});return}let a={slack:buildSlackWorkspaceHandle({api:r.api,botToken:r.config.credentials?.botToken,installationTeamId:t,teamId:e.teamId})};dispatchInteractionHook(()=>i(e,a),n,`shortcut handler failed`)}function dispatchInteractionHook(e,t,n){t.waitUntil(Promise.resolve().then(e).catch(e=>{log.error(n,{error:e})}))}async function dispatchBlockAnswers(e){let t=e.route??{channelId:e.interaction.channelId,threadTs:e.interaction.threadTs},n=await authorizeAnswer({deps:e.deps,installationTeamId:e.interaction.installationTeamId,submission:e.submission,target:t,teamId:e.interaction.teamId});if(n===null)return;let r=await respondOrRetireInactivePrompt(()=>e.ctx.from(slackSessionToken(t)).respond(e.submission.answers,{auth:n.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleHitlCard(e.interaction,e.deps,{label:t,text:t})},log);r.outcome===`inactive`&&await Promise.all([r.retirement,postEphemeralNotice({channelId:e.interaction.channelId,deps:e.deps,installationTeamId:e.interaction.installationTeamId,teamId:e.interaction.teamId,threadTs:e.interaction.threadTs,userId:e.submission.user.id})])}async function postEphemeralNotice(e){let{thread:t}=buildSlackBinding({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,channelId:e.channelId,installationTeamId:e.installationTeamId,teamId:e.teamId,threadTs:e.threadTs});try{await t.postEphemeral(e.userId,INACTIVE_REQUEST_NOTICE)}catch(e){log.error(`inactive prompt notice failed`,{error:e})}}async function openFreeformModal(e){let t=e.payload.trigger_id;if(typeof t!=`string`||t.length===0){log.warn(`freeform button click missing trigger_id — cannot open modal`);return}let n=decodeFreeformHitlActionId(e.freeformAction.actionId);if(n===null){log.warn(`freeform button click carries invalid metadata`);return}let r=n.requestId;if(!r){log.warn(`freeform button click missing requestId`);return}let i=e.freeformAction.messageTs;if(!i){log.warn(`freeform button click missing messageTs`);return}let a=n.route??{channelId:e.interaction.channelId,threadTs:e.interaction.threadTs},o={...a,continuationToken:slackSessionToken(a),installationTeamId:e.interaction.installationTeamId,messageChannelId:e.interaction.channelId,messageTs:i,requestId:r},s=readPromptTextFromBlocks(e.interaction.messageBlocks),c=buildFreeformModalView({metadata:o,prompt:s}),l=await resolveSlackBotToken(e.deps.config.credentials?.botToken,{teamId:e.interaction.installationTeamId});try{await callSlackApi({api:e.deps.api,botToken:l,operation:`views.open`,body:{trigger_id:t,view:c}})}catch(e){if(!isSlackResponseError(e))throw e;log.error(`Slack views.open failed`,{error:e})}}async function handleViewSubmission(e,t,n){let r=new Response(null,{status:200});if(e.callbackId!==HITL_FREEFORM_MODAL_CALLBACK_ID)return r;let i;try{i=JSON.parse(e.privateMetadata??``)}catch{return log.warn(`freeform view_submission carries invalid private_metadata`),r}if(!i.continuationToken||!i.requestId||!i.messageTs||!i.channelId||i.codeChannel!==!0&&!i.threadTs)return r;let a=e.values?.find(e=>e.blockId===HITL_FREEFORM_MODAL_BLOCK_ID&&e.actionId===HITL_FREEFORM_MODAL_ACTION_ID)?.value??``;if(a.length===0)return r;let o=e.user,s=e.userId,c=o?.teamId??e.teamId??null,l=i.installationTeamId??readInstallationTeamId(e.raw),u={type:`view_submission`,answers:[parseAnswer({interactionId:i.requestId,text:a})],messageTs:i.messageTs,user:{id:s,username:o?.username,name:o?.name}};return t.waitUntil(dispatchViewAnswer({ctx:t,deps:n,installationTeamId:l,metadata:i,submission:u,teamId:c})),r}async function dispatchViewAnswer(e){let{channelId:t,threadTs:n}=e.metadata,r=await authorizeAnswer({deps:e.deps,installationTeamId:e.installationTeamId,submission:e.submission,target:e.metadata,teamId:e.teamId});if(r===null)return;let i=e.metadata.messageChannelId??t,a=await respondOrRetireInactivePrompt(()=>e.ctx.from(e.metadata.continuationToken).respond(e.submission.answers,{auth:r.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleFreeformCard({card:{label:t,text:t},channelId:i,deps:e.deps,installationTeamId:e.installationTeamId,messageTs:e.metadata.messageTs})},log);a.outcome===`inactive`&&await Promise.all([a.retirement,postEphemeralNotice({channelId:i,deps:e.deps,installationTeamId:e.installationTeamId,teamId:e.teamId??void 0,threadTs:i===t?n??``:``,userId:e.submission.user.id})])}export{handleInteractionPost,parseBlockActionsPayload,parseShortcutPayload};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ToolContext } from "#tools/definition.js";
|
|
2
|
+
import type { SlackChannel, SlackOpenCodeChannelOptions } from "./slackChannel.js";
|
|
3
|
+
/** What {@link openCodeChannel} returns to the tool that calls it. */
|
|
4
|
+
export type SlackCodeChannelOpenResult = {
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly status: "opened";
|
|
7
|
+
} | {
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
readonly status: "unavailable";
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Forks the Slack conversation that called a tool into a new Slack code
|
|
13
|
+
* channel. The code channel's session starts with the conversation the model
|
|
14
|
+
* saw, then `message`. Slack links the channel in the conversation, and people
|
|
15
|
+
* continue the work there.
|
|
16
|
+
*
|
|
17
|
+
* Call it from your own tool, passing your Slack channel, which needs
|
|
18
|
+
* `codeChannels: true`:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* async execute({ message, name }, ctx) {
|
|
22
|
+
* return await openCodeChannel(ctx, slack, message, { name });
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Returns `unavailable` when the conversation isn't a Slack message or is
|
|
27
|
+
* already a code channel. When Slack refuses, it throws with Slack's error. A
|
|
28
|
+
* retried call reaches the same code channel and session once.
|
|
29
|
+
*/
|
|
30
|
+
export declare function openCodeChannel(ctx: Pick<ToolContext, "callId" | "channel" | "messages" | "session" | "to">, slack: SlackChannel, message: string, options?: SlackOpenCodeChannelOptions): Promise<SlackCodeChannelOpenResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function openCodeChannel(e,t,n,r={}){let{channelId:i,codeChannel:a,installationTeamId:o,threadTs:s,triggeringMessageTs:c}=e.channel.metadata??{};return a===!0?unavailable(`This conversation is already a code channel.`):typeof i!=`string`||typeof s!=`string`?unavailable(`This conversation is not a Slack message.`):{sessionId:(await e.to(t,{channelId:i,codeChannel:{...r.name!==void 0&&{name:r.name},originThreadTs:s,originTs:typeof c==`string`?c:s},...typeof o==`string`&&{installationTeamId:o}}).send(n,{auth:e.session.auth.current,history:textHistory(e.messages),idempotencyKey:`${e.session.id}:${e.callId}`})).sessionId,status:`opened`}}function unavailable(e){return{reason:e,status:`unavailable`}}function textHistory(e){return e.flatMap(e=>{if(e.role!==`user`&&e.role!==`assistant`)return[];let t=typeof e.content==`string`?e.content:e.content.flatMap(e=>e.type===`text`?[e.text]:[]).join(``);return t===``?[]:[{role:e.role,text:t}]})}export{openCodeChannel};
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import type { SlackHandle } from "#public/channels/slack/api.js";
|
|
2
|
-
import type {
|
|
3
|
-
import type
|
|
2
|
+
import type { BlockKitBlock } from "#public/channels/slack/blocks.js";
|
|
3
|
+
import { type RenderedRequestCard } from "#public/channels/slack/request-card-render.js";
|
|
4
4
|
type PrivateApprovalSlack = Pick<SlackHandle, "channelId" | "request" | "threadTs">;
|
|
5
|
+
/** Where a direct-message request's messages landed. */
|
|
6
|
+
export interface PrivateInteractionDelivery {
|
|
7
|
+
readonly channelId: string;
|
|
8
|
+
readonly ts: string;
|
|
9
|
+
readonly detailsTs?: string;
|
|
10
|
+
/** The controls blocks posted: the rendered ones, or eve's default if Slack rejected them. */
|
|
11
|
+
readonly blocks: readonly BlockKitBlock[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Posts one rendered request to `reviewer`'s DM: a permalink to the thread, its details, then
|
|
15
|
+
* its controls. A failure deletes what it posted, so a reviewer never sees half a request.
|
|
16
|
+
*/
|
|
5
17
|
export declare function deliverPrivateInteraction(input: {
|
|
6
18
|
readonly previewMessageTs: string;
|
|
7
|
-
readonly
|
|
19
|
+
readonly card: RenderedRequestCard;
|
|
8
20
|
readonly reviewer: string;
|
|
9
21
|
readonly slack: PrivateApprovalSlack;
|
|
10
|
-
}): Promise<
|
|
22
|
+
}): Promise<PrivateInteractionDelivery>;
|
|
11
23
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger,logError}from"#internal/logging.js";import{
|
|
1
|
+
import{createLogger,logError}from"#internal/logging.js";import{sendWithFallback}from"#public/channels/slack/request-card-render.js";const log=createLogger(`slack.private-approval-delivery`);async function deliverPrivateInteraction(e){let t=await e.slack.request(`conversations.open`,{users:e.reviewer}),n=t.ok===!0?t.channel?.id:void 0;if(typeof n!=`string`||n.length===0)throw Error(`Slack conversations.open failed: ${t.error??`unknown_error`}`);let{fallback:r,parts:i}=e.card,a=[],post=async t=>{let r=await postMessage(e.slack,{channel:n,unfurl_links:!1,unfurl_media:!1,...t});return a.push(r),r},postPart=async e=>{let t=await post({blocks:e.blocks,text:e.text});return{blocks:e.blocks,ts:t}};try{let t=await resolveMessagePermalink(e.slack,e.previewMessageTs);t!==void 0&&await post({markdown_text:t,unfurl_links:!0});let a=i.details===void 0?void 0:await sendWithFallback(postPart,i.details,r.details),o=await sendWithFallback(postPart,i.controls,r.controls),s={blocks:o.blocks,channelId:n,ts:o.ts};return a!==void 0&&(s.detailsTs=a.ts),s}catch(t){throw await Promise.allSettled(a.map(async t=>{try{await e.slack.request(`chat.delete`,{channel:n,ts:t})}catch(e){logError(log,`failed to roll back partial private approval delivery`,e,{channelId:n,messageTs:t})}})),t}}async function resolveMessagePermalink(e,t){if(!e.channelId||!t)return;let n=await e.request(`chat.getPermalink`,{channel:e.channelId,message_ts:t});return n.ok===!0&&typeof n.permalink==`string`?n.permalink:void 0}async function postMessage(e,t){let n=await e.request(`chat.postMessage`,t);if(n.ok!==!0||typeof n.ts!=`string`||n.ts.length===0)throw Object.assign(Error(`Slack chat.postMessage failed: ${n.error??`unknown_error`}`),{response:n});return n.ts}export{deliverPrivateInteraction};
|