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,10 +1,13 @@
|
|
|
1
|
+
import type { Interaction } from "#channel/interaction-prompts.js";
|
|
1
2
|
import type { TaskCard } from "#channel/task-card.js";
|
|
3
|
+
import type { SessionView } from "#protocol/session-projection/session-view.js";
|
|
4
|
+
import type { ChannelAudience } from "#shared/channel-audience.js";
|
|
2
5
|
import type { ChannelEventContext, ChannelEventOf } from "#public/definitions/channel.js";
|
|
3
6
|
import type { BlockKitBlock } from "#public/channels/slack/blocks.js";
|
|
4
7
|
import type { SlackMessage } from "#public/channels/slack/inbound.js";
|
|
5
|
-
import type { SlackAuthorizationEventContext, SlackChannelInternalEvents, SlackContext, SlackEventContext } from "#public/channels/slack/slackChannel.js";
|
|
8
|
+
import type { SlackApprovalChannel, SlackAuthorizationEventContext, SlackChannelInternalEvents, SlackContext, SlackEventContext } from "#public/channels/slack/slackChannel.js";
|
|
6
9
|
/** The session events a Slack renderer can handle, in no particular order. */
|
|
7
|
-
declare const SLACK_RENDERED_EVENTS: readonly ["call.progress", "call.requested", "call.started", "call.settled", "content.completed", "content.delta", "delivery.settled", "interaction.opened", "interaction.settled", "model.settled", "model.started", "response.settled", "session.ended", "task.ended", "task.started", "turn.paused", "turn.settled", "turn.started"];
|
|
10
|
+
declare const SLACK_RENDERED_EVENTS: readonly ["call.progress", "call.requested", "call.started", "call.settled", "content.completed", "content.delta", "delivery.settled", "interaction.opened", "interaction.settled", "model.settled", "model.started", "response.admitted", "response.settled", "session.ended", "task.ended", "task.started", "turn.paused", "turn.settled", "turn.started"];
|
|
8
11
|
export type SlackRenderedEvent = (typeof SLACK_RENDERED_EVENTS)[number];
|
|
9
12
|
type SlackSessionEvent = Exclude<SlackRenderedEvent, "interaction.opened">;
|
|
10
13
|
/**
|
|
@@ -26,11 +29,47 @@ export type SlackRendererEvents = {
|
|
|
26
29
|
} & {
|
|
27
30
|
readonly "interaction.opened"?: (event: ChannelEventOf<"interaction.opened">, ctx: ChannelEventContext<SlackEventContext | SlackAuthorizationEventContext>, next: SlackRenderNext<"interaction.opened">) => void | Promise<void>;
|
|
28
31
|
};
|
|
29
|
-
/**
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* One Slack message a renderer hook returns, such as a task card or an input request's controls:
|
|
34
|
+
* Block Kit blocks and notification text.
|
|
35
|
+
*/
|
|
36
|
+
export interface SlackCardMessage {
|
|
31
37
|
readonly blocks: readonly BlockKitBlock[];
|
|
32
38
|
readonly text: string;
|
|
33
39
|
}
|
|
40
|
+
/** One input request as eve is about to post or update it. */
|
|
41
|
+
export interface SlackInteractionCard {
|
|
42
|
+
/**
|
|
43
|
+
* The request, read from the session: `status`, `outcome`, `call` (with `input`), and `answer`.
|
|
44
|
+
* Once settled, `answer` is the answer that decided it. While open, it is the latest admitted
|
|
45
|
+
* answer, if any; an answer still being checked, or one that was refused, doesn't count.
|
|
46
|
+
*/
|
|
47
|
+
readonly interaction: Interaction;
|
|
48
|
+
/**
|
|
49
|
+
* Where eve posts it, as `approvalChannel` decided. In a code channel, `"thread"` means the code
|
|
50
|
+
* channel itself.
|
|
51
|
+
*/
|
|
52
|
+
readonly destination: SlackApprovalChannel;
|
|
53
|
+
/**
|
|
54
|
+
* Who reads the message. A direct message is always `private`; a thread request has the
|
|
55
|
+
* thread's audience. Same meaning as `TaskCard.audience`.
|
|
56
|
+
*/
|
|
57
|
+
readonly audience: ChannelAudience;
|
|
58
|
+
/** The Slack user whose answer stands or decided the request, when known. */
|
|
59
|
+
readonly answeredBy?: string;
|
|
60
|
+
/** Session snapshot, for the `eve/events` selectors. */
|
|
61
|
+
readonly view: SessionView;
|
|
62
|
+
}
|
|
63
|
+
/** The messages of one input request. */
|
|
64
|
+
export interface SlackInteractionCardParts {
|
|
65
|
+
/** The message holding eve's controls. Slack echoes it back on every click, so keep it small. */
|
|
66
|
+
readonly controls: SlackCardMessage;
|
|
67
|
+
/**
|
|
68
|
+
* Posted as a separate message before the controls, for any kind of request. Requests asked
|
|
69
|
+
* together share one details message, which eve deletes once its last open request settles.
|
|
70
|
+
*/
|
|
71
|
+
readonly details?: SlackCardMessage;
|
|
72
|
+
}
|
|
34
73
|
/**
|
|
35
74
|
* One link in a Slack channel's rendering chain. Every part is optional and
|
|
36
75
|
* wraps the renderers after it, ending with eve's default renderer.
|
|
@@ -53,7 +92,17 @@ export interface SlackRenderer {
|
|
|
53
92
|
* turn changes, and handles Slack's rate limits. `next` returns eve's default
|
|
54
93
|
* card, which is `null` for a card without tasks.
|
|
55
94
|
*/
|
|
56
|
-
readonly taskCard?: (card: TaskCard, next: (card: TaskCard) =>
|
|
95
|
+
readonly taskCard?: (card: TaskCard, next: (card: TaskCard) => SlackCardMessage | null) => SlackCardMessage | null;
|
|
96
|
+
/**
|
|
97
|
+
* Returns the messages for one approval, question, or budget prompt. Runs when the request
|
|
98
|
+
* opens, and again when an answer is admitted or refused and when it settles. Pure and
|
|
99
|
+
* synchronous: eve keeps routing, batching, action IDs, and posting. While the request is open
|
|
100
|
+
* and unanswered, `controls` must hold exactly the `eve_input:` controls `next` renders, each
|
|
101
|
+
* once; once it is answered or settled, it must hold none. `details` never holds them, and each
|
|
102
|
+
* message holds at most 50 blocks. A result that breaks these rules, or a hook that throws,
|
|
103
|
+
* falls back to eve's default for that request.
|
|
104
|
+
*/
|
|
105
|
+
readonly interactionCard?: (card: SlackInteractionCard, next: (card: SlackInteractionCard) => SlackInteractionCardParts) => SlackInteractionCardParts;
|
|
57
106
|
}
|
|
58
107
|
/** Types a Slack renderer. */
|
|
59
108
|
export declare function defineSlackRenderer(renderer: SlackRenderer): SlackRenderer;
|
|
@@ -64,7 +113,10 @@ export declare function defineSlackRenderer(renderer: SlackRenderer): SlackRende
|
|
|
64
113
|
export interface SlackRenderChain {
|
|
65
114
|
readonly events: SlackChannelInternalEvents;
|
|
66
115
|
readonly received: (message: SlackMessage, channel: SlackContext) => Promise<void>;
|
|
67
|
-
readonly taskCard: (card: TaskCard) =>
|
|
116
|
+
readonly taskCard: (card: TaskCard) => SlackCardMessage | null;
|
|
68
117
|
}
|
|
69
118
|
export declare function composeSlackRenderers(renderers: readonly SlackRenderer[], defaults: SlackRenderChain): SlackRenderChain;
|
|
119
|
+
/** Renderers' `interactionCard` hooks, first outermost, around `last`: eve's default. */
|
|
120
|
+
export type SlackInteractionCardChain = (card: SlackInteractionCard, last: (card: SlackInteractionCard) => SlackInteractionCardParts) => SlackInteractionCardParts;
|
|
121
|
+
export declare function composeInteractionCards(renderers: readonly SlackRenderer[]): SlackInteractionCardChain;
|
|
70
122
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{removedEventKeyMessage}from"#public/definitions/removed-event-keys.js";const SLACK_RENDERED_EVENTS=[`call.progress`,`call.requested`,`call.started`,`call.settled`,`content.completed`,`content.delta`,`delivery.settled`,`interaction.opened`,`interaction.settled`,`model.settled`,`model.started`,`response.settled`,`session.ended`,`task.ended`,`task.started`,`turn.paused`,`turn.settled`,`turn.started`];function defineSlackRenderer(e){for(let t of Object.keys(e.events??{})){let e=removedEventKeyMessage(t);if(e!==void 0)throw Error(`A Slack renderer handles ${e}`)}return e}function composeSlackRenderers(e,t){for(let t of e)defineSlackRenderer(t);return{events:composeEvents(e,t.events),received:(n,r)=>composeReceived(e,t,n,r,0),taskCard:n=>composeTaskCard(e,t,n,0)}}function composeEvents(e,t){let n={};for(let r of SLACK_RENDERED_EVENTS){let i=t[r],a=e.flatMap(e=>{let t=e.events?.[r];return t===void 0?[]:[t]});(a.length!==0||i!==void 0)&&(n[r]=(e,t)=>runChain(a,e,(e,n,i)=>e(n,userContext(r,t,n),i),e=>i?.(e,t)))}return n}async function runChain(e,t,n,r){let run=async(t,i)=>{if(t===e.length){await r(i);return}let a;await n(e[t],i,e=>a??=run(t+1,e===void 0?i:e))};await run(0,t)}function userContext(e,t,n){if(e!==`interaction.opened`||!isSignIn(n))return t;let{channel:r}=t,i={postDirectMessage:(e,t)=>r.thread.postDirectMessage(e,t),postEphemeral:(e,t)=>r.thread.postEphemeral(e,t),state:r.state};return{...t,channel:i}}function isSignIn(e){return e?.data?.request?.kind===`sign-in`}async function composeReceived(e,t,n,r,i){if(i===e.length){await t.received(n,r);return}let a=e[i].received;if(a===void 0){await composeReceived(e,t,n,r,i+1);return}let o;await a(n,r,()=>(o??=composeReceived(e,t,n,r,i+1),o))}function composeTaskCard(e,t,n,r){if(r===e.length)return t.taskCard(n);let i=e[r].taskCard;return i===void 0?composeTaskCard(e,t,n,r+1):i(n,n=>composeTaskCard(e,t,n,r+1))}export{composeSlackRenderers,defineSlackRenderer};
|
|
1
|
+
import{removedEventKeyMessage}from"#public/definitions/removed-event-keys.js";const SLACK_RENDERED_EVENTS=[`call.progress`,`call.requested`,`call.started`,`call.settled`,`content.completed`,`content.delta`,`delivery.settled`,`interaction.opened`,`interaction.settled`,`model.settled`,`model.started`,`response.admitted`,`response.settled`,`session.ended`,`task.ended`,`task.started`,`turn.paused`,`turn.settled`,`turn.started`];function defineSlackRenderer(e){for(let t of Object.keys(e.events??{})){let e=removedEventKeyMessage(t);if(e!==void 0)throw Error(`A Slack renderer handles ${e}`)}return e}function composeSlackRenderers(e,t){for(let t of e)defineSlackRenderer(t);return{events:composeEvents(e,t.events),received:(n,r)=>composeReceived(e,t,n,r,0),taskCard:n=>composeTaskCard(e,t,n,0)}}function composeEvents(e,t){let n={};for(let r of SLACK_RENDERED_EVENTS){let i=t[r],a=e.flatMap(e=>{let t=e.events?.[r];return t===void 0?[]:[t]});(a.length!==0||i!==void 0)&&(n[r]=(e,t)=>runChain(a,e,(e,n,i)=>e(n,userContext(r,t,n),i),e=>i?.(e,t)))}return n}async function runChain(e,t,n,r){let run=async(t,i)=>{if(t===e.length){await r(i);return}let a;await n(e[t],i,e=>a??=run(t+1,e===void 0?i:e))};await run(0,t)}function userContext(e,t,n){if(e!==`interaction.opened`||!isSignIn(n))return t;let{channel:r}=t,i={postDirectMessage:(e,t)=>r.thread.postDirectMessage(e,t),postEphemeral:(e,t)=>r.thread.postEphemeral(e,t),state:r.state};return{...t,channel:i}}function isSignIn(e){return e?.data?.request?.kind===`sign-in`}async function composeReceived(e,t,n,r,i){if(i===e.length){await t.received(n,r);return}let a=e[i].received;if(a===void 0){await composeReceived(e,t,n,r,i+1);return}let o;await a(n,r,()=>(o??=composeReceived(e,t,n,r,i+1),o))}function composeInteractionCards(e){let t=e.flatMap(e=>e.interactionCard??[]);return(e,n)=>{let run=(e,r)=>e===t.length?n(r):t[e](r,t=>run(e+1,t));return run(0,e)}}function composeTaskCard(e,t,n,r){if(r===e.length)return t.taskCard(n);let i=e[r].taskCard;return i===void 0?composeTaskCard(e,t,n,r+1):i(n,n=>composeTaskCard(e,t,n,r+1))}export{composeInteractionCards,composeSlackRenderers,defineSlackRenderer};
|
|
@@ -56,6 +56,10 @@ export declare function postCompletedSlackReply(channel: SlackContext, reply: Sl
|
|
|
56
56
|
export declare function deliverCompletedSlackReply(channel: SlackContext, reply: SlackCompletedReply, logFields: {
|
|
57
57
|
readonly turnId?: string;
|
|
58
58
|
}): Promise<void>;
|
|
59
|
+
/** Whether Slack's error `code` means it refused the message's content and posted nothing. */
|
|
60
|
+
export declare function isRefusedPayloadCode(code: unknown): code is string;
|
|
61
|
+
/** Slack's error code when `error` is a refused-payload error, checking its causes. */
|
|
62
|
+
export declare function refusedPayloadError(error: unknown): string | undefined;
|
|
59
63
|
/**
|
|
60
64
|
* Wraps a channel's composed `content.completed` chain, authored renderers
|
|
61
65
|
* included, so a final reply that fails is never lost without a trace.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"#internal/logging.js";import{replyTextOf}from"#public/channels/reply.js";import{reportUndeliveredReply}from"#public/channels/undelivered-reply.js";import{SLACK_MARKDOWN_TEXT_MAX_LENGTH,truncateMessageText}from"#public/channels/slack/limits.js";const log=createLogger(`slack.reply`),LONG_RESPONSE_NOTICE=`Here's a snippet with the full response.`,FALLBACK_LOG=`Slack refused the final reply; delivering it with a fallback`,REFUSED_PAYLOAD_ERRORS=new Set([`invalid_blocks`,`invalid_blocks_format`,`msg_blocks_too_long`,`msg_blocks_too_many`,`msg_too_long`]);async function postCompletedSlackReply(e,t){await deliverCompletedSlackReply(e,t,{})}async function deliverCompletedSlackReply(e,t,n){let{blocks:r,markdown:i,text:a}=typeof t==`string`?{markdown:t}:t;if(await threadRootIsGone(e)){log.warn(`Slack thread root was deleted; dropping the final reply`,n);return}let o;if(r!==void 0)try{await e.thread.post({blocks:r,text:a??truncateMessageText(i)});return}catch(e){if(o=refusedPayloadError(e),o===void 0)throw e}if(i.length<=SLACK_MARKDOWN_TEXT_MAX_LENGTH){o!==void 0&&log.warn(FALLBACK_LOG,{fallback:`inline`,slackError:o,...n});try{await e.thread.post(i);return}catch(e){if(o=refusedPayloadError(e),o===void 0)throw e}}o!==void 0&&log.warn(FALLBACK_LOG,{fallback:`snippet`,slackError:o,...n}),await uploadReplySnippet(e,i)}async function threadRootIsGone(e){let{channelId:t,threadTs:
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{replyTextOf}from"#public/channels/reply.js";import{reportUndeliveredReply}from"#public/channels/undelivered-reply.js";import{SLACK_MARKDOWN_TEXT_MAX_LENGTH,truncateMessageText}from"#public/channels/slack/limits.js";const log=createLogger(`slack.reply`),LONG_RESPONSE_NOTICE=`Here's a snippet with the full response.`,FALLBACK_LOG=`Slack refused the final reply; delivering it with a fallback`,REFUSED_PAYLOAD_ERRORS=new Set([`invalid_blocks`,`invalid_blocks_format`,`msg_blocks_too_long`,`msg_blocks_too_many`,`msg_too_long`]);async function postCompletedSlackReply(e,t){await deliverCompletedSlackReply(e,t,{})}async function deliverCompletedSlackReply(e,t,n){let{blocks:r,markdown:i,text:a}=typeof t==`string`?{markdown:t}:t;if(await threadRootIsGone(e)){log.warn(`Slack thread root was deleted; dropping the final reply`,n);return}let o;if(r!==void 0)try{await e.thread.post({blocks:r,text:a??truncateMessageText(i)});return}catch(e){if(o=refusedPayloadError(e),o===void 0)throw e}if(i.length<=SLACK_MARKDOWN_TEXT_MAX_LENGTH){o!==void 0&&log.warn(FALLBACK_LOG,{fallback:`inline`,slackError:o,...n});try{await e.thread.post(i);return}catch(e){if(o=refusedPayloadError(e),o===void 0)throw e}}o!==void 0&&log.warn(FALLBACK_LOG,{fallback:`snippet`,slackError:o,...n}),await uploadReplySnippet(e,i)}async function threadRootIsGone(e){let{channelId:t,codeChannel:n,threadTs:r}=e.slack;if(!t||!r||n||e.state?.postedInThread===!0)return!1;try{let n=await e.slack.request(`chat.getPermalink`,{channel:t,message_ts:r});return n.ok!==!0&&n.error===`message_not_found`}catch{return!1}}async function uploadReplySnippet(e,t){let n={data:new Blob([t],{type:`text/markdown`}),filename:`eve-response.md`,mimeType:`text/markdown`},r=e.slack.codeChannel||e.slack.threadTs.length>0;if(!r&&(!(await e.thread.post(LONG_RESPONSE_NOTICE)).id||e.slack.threadTs.length===0))throw Error(`Slack did not return a thread timestamp for the long response notice.`);await e.slack.uploadFiles([n],{initialComment:r?LONG_RESPONSE_NOTICE:void 0,snippetType:`markdown`})}function isRefusedPayloadCode(e){return typeof e==`string`&&REFUSED_PAYLOAD_ERRORS.has(e)}function refusedPayloadError(e){let t=e;for(let e=0;e<5&&typeof t==`object`&&t;e++){let e=t,n=e.response?.error??e.data?.error;if(isRefusedPayloadCode(n))return n;t=e.cause}}function withFinalReplyDelivery(e){return async(t,n)=>{if(replyTextOf(t.data)===void 0){await e?.(t,n);return}try{await e?.(t,n)}catch(e){let{channel:t}=n;await reportUndeliveredReply({log,name:`Slack`,post:e=>t.thread.post(e),state:t.state},n.session.turn.id,e)}}}export{deliverCompletedSlackReply,isRefusedPayloadCode,postCompletedSlackReply,refusedPayloadError,withFinalReplyDelivery};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders one approval, question, or budget prompt through the renderers' `interactionCard`
|
|
3
|
+
* chain, and checks the result against eve's own controls so a renderer can restyle a card but
|
|
4
|
+
* never break how its clicks route.
|
|
5
|
+
*/
|
|
6
|
+
import type { Interaction } from "#channel/interaction-prompts.js";
|
|
7
|
+
import { type SlackHitlRoute } from "#public/channels/slack/hitl.js";
|
|
8
|
+
import type { SlackCardMessage, SlackInteractionCard, SlackInteractionCardChain, SlackInteractionCardParts } from "#public/channels/slack/renderers.js";
|
|
9
|
+
/** One request's messages, and eve's default for it, sent when Slack rejects the rendered ones. */
|
|
10
|
+
export interface RenderedRequestCard {
|
|
11
|
+
readonly parts: SlackInteractionCardParts;
|
|
12
|
+
readonly fallback: SlackInteractionCardParts;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* eve's default card: an open, unanswered request's controls, or once answered or settled, one
|
|
16
|
+
* block naming the answer and, when known, who gave it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defaultInteractionCard(card: SlackInteractionCard, route?: SlackHitlRoute): SlackInteractionCardParts;
|
|
19
|
+
/**
|
|
20
|
+
* How an answered or settled request reads: the answer's label, or how it ended without one,
|
|
21
|
+
* and the notification text. The card and a direct-message request's thread note share it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function endingOf(interaction: Interaction): {
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly text: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Runs `chain` for one request, falling back to eve's default when the renderer throws or its
|
|
29
|
+
* result fails {@link invalidCardReason}. The default renders from the original interaction, so
|
|
30
|
+
* a changed card handed to `next` can't change which controls are expected.
|
|
31
|
+
*/
|
|
32
|
+
export declare function renderRequestCard(chain: SlackInteractionCardChain, card: SlackInteractionCard, route?: SlackHitlRoute): RenderedRequestCard;
|
|
33
|
+
/**
|
|
34
|
+
* eve's default at its smallest: only the block holding its controls, which every open default
|
|
35
|
+
* has exactly one of, or the single block of an answered or settled default.
|
|
36
|
+
*/
|
|
37
|
+
export declare function compactDefault(fallback: SlackCardMessage): SlackCardMessage;
|
|
38
|
+
/**
|
|
39
|
+
* Sends `message`, and once more with eve's default when Slack refuses its content, unless the
|
|
40
|
+
* default is the same message. Any other failure is thrown, since resending after a timeout could
|
|
41
|
+
* duplicate a message that landed. A request without a default for this message, such as details
|
|
42
|
+
* a renderer added, sends nothing the second time.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sendWithFallback<T>(send: (message: SlackCardMessage) => Promise<T>, message: SlackCardMessage, fallback: SlackCardMessage | undefined): Promise<T | undefined>;
|
|
45
|
+
export declare function sameJson(a: unknown, b: unknown): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createLogger,logError}from"#internal/logging.js";import{truncate}from"#public/channels/text.js";import{SLACK_MAX_BLOCKS_PER_MESSAGE,SLACK_SECTION_TEXT_MAX_LENGTH}from"#public/channels/slack/limits.js";import{refusedPayloadError}from"#public/channels/slack/reply-delivery.js";import{isFreeformAction,isHitlAction,renderInteractionPostParts}from"#public/channels/slack/hitl.js";import{answeredBlocksFromActionBlock}from"#public/channels/slack/interaction-cards.js";const log=createLogger(`slack.interaction-card`);function defaultInteractionCard(e,t){let{interaction:n}=e,r=renderInteractionPostParts(n,t);if(n.status===`open`&&n.answer===void 0)return r;let{label:i,text:a}=endingOf(n);return{controls:{blocks:[answeredBlocksFromActionBlock({answerLabel:i,block:r.controls.blocks[0],userId:e.answeredBy})?.[0]??answeredSection(n.request.prompt,i,e.answeredBy)],text:a}}}function endingOf(e){let{answer:t,outcome:n,request:r}=e;if(t!==void 0){let e=r.options?.find(e=>e.id===t.optionId)?.label??t.text??t.optionId??`Answered`;return{label:e,text:`Answered: ${e}`}}let i=n===`accepted`?`Accepted`:n===`declined`?`Declined`:`No longer needed`;return{label:i,text:i}}function answeredSection(e,t,n){let r=n===void 0?``:` by <@${n}>`,i=`:white_check_mark: *${truncate(t,500)}*${r}`;return{text:{text:`${truncate(e,SLACK_SECTION_TEXT_MAX_LENGTH-i.length-1)}\n${i}`,type:`mrkdwn`},type:`section`}}function renderRequestCard(e,t,n){let r=defaultInteractionCard(t,n),{interactionId:i}=t.interaction;try{let a=JSON.parse(JSON.stringify(e(t,e=>defaultInteractionCard(e,n)))),o=invalidCardReason(a,r);if(o===void 0)return{fallback:r,parts:a};log.warn(`interactionCard result rejected; using eve's default`,{interactionId:i,problem:o})}catch(e){logError(log,`interactionCard renderer failed; using eve's default`,e,{interactionId:i})}return{fallback:r,parts:r}}function invalidCardReason(e,t){for(let[t,n]of[[`controls`,e.controls],[`details`,e.details]])if(n!==void 0||t!==`details`){if(!isMessage(n))return`${t} is not a message with blocks and text`;if(n.blocks.length>SLACK_MAX_BLOCKS_PER_MESSAGE)return`${t} has more than ${SLACK_MAX_BLOCKS_PER_MESSAGE} blocks`}if(e.details!==void 0&&eveControls(e.details.blocks).length>0)return`details hold eve controls`;let n=eveControls(t.controls.blocks),r=eveControls(e.controls.blocks);if(n.length!==r.length||n.some((e,t)=>e!==r[t]))return n.length===0?`controls of an answered or settled request hold eve controls`:`controls don't hold exactly eve's controls`}function isMessage(e){if(typeof e!=`object`||!e)return!1;let{blocks:t,text:n}=e;return Array.isArray(t)&&typeof n==`string`}function compactDefault(e){let t=e.blocks.filter(e=>eveControls(e).length>0);return t.length===0?e:{...e,blocks:t}}function eveControls(e){let t=[],walk=e=>{if(Array.isArray(e)){for(let t of e)walk(t);return}if(typeof e!=`object`||!e)return;let n=e,r=n.action_id;if(typeof r==`string`&&(isHitlAction(r)||isFreeformAction(r))){let e=Array.isArray(n.options)?n.options.map(e=>e.value).sort():null;t.push(JSON.stringify([r,n.value??null,e]))}for(let t of Object.values(e))walk(t)};return walk(e),t.sort()}async function sendWithFallback(e,t,n){try{return await e(t)}catch(r){if(refusedPayloadError(r)===void 0||sameJson(t,n))throw r;return logError(log,`Slack rejected a rendered input request; sending eve's default`,r),n===void 0?void 0:await e(n)}}function sameJson(e,t){return JSON.stringify(e)===JSON.stringify(t)}export{compactDefault,defaultInteractionCard,endingOf,renderRequestCard,sameJson,sendWithFallback};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eve's Slack cards for approvals, questions, and budget prompts: a commit's requests post as one
|
|
3
|
+
* batch, and each card updates as its request is answered and settles. Renderers choose the
|
|
4
|
+
* blocks through `interactionCard`; this module owns routing, batching, and the messages.
|
|
5
|
+
*/
|
|
6
|
+
import { type RefusedAnswer, type RequestBatch } from "#channel/interaction-prompts.js";
|
|
7
|
+
import type { ChannelEventContext } from "#public/definitions/channel.js";
|
|
8
|
+
import type { SlackInteractionCardChain } from "#public/channels/slack/renderers.js";
|
|
9
|
+
import type { SlackApprovalChannelResolver, SlackEventContext } from "#public/channels/slack/slackChannel.js";
|
|
10
|
+
type RequestCardContext = ChannelEventContext<SlackEventContext>;
|
|
11
|
+
/**
|
|
12
|
+
* Posts a commit's requests: each `approvalChannel` sends to the thread joins one batch, details
|
|
13
|
+
* first, then controls, grouped under Slack's 50-block cap; each sent to a DM posts there alone.
|
|
14
|
+
*/
|
|
15
|
+
export declare function postRequestCards(batch: RequestBatch, ctx: RequestCardContext, chain: SlackInteractionCardChain, approvalChannel: SlackApprovalChannelResolver | undefined): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Shows an open request's changed answer: an admitted one, or the controls again once it was
|
|
18
|
+
* refused. A request already settled in the commit is left to `interaction.settled`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function showAnswerChange(interactionId: string, ctx: RequestCardContext, chain: SlackInteractionCardChain): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Writes a settled request's card. Every request of its message that settled in the same commit
|
|
23
|
+
* without its settled rendering written joins the one update, since the commit's view already
|
|
24
|
+
* shows them settled. Then deletes each details message whose requests have all written their
|
|
25
|
+
* settled rendering, reports the outcome in a DM request's thread note, and forgets the message
|
|
26
|
+
* once every request in it has.
|
|
27
|
+
*/
|
|
28
|
+
export declare function settleRequestCard(interactionId: string, ctx: RequestCardContext, chain: SlackInteractionCardChain): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* A responder an approval's policy or check refused hears why privately. A pending answer gets
|
|
31
|
+
* no notice: an ephemeral cannot be removed once the approval settles, and the card itself
|
|
32
|
+
* reports the outcome moments later.
|
|
33
|
+
*/
|
|
34
|
+
export declare function notifyRefusedResponder(refused: RefusedAnswer, channel: SlackEventContext): Promise<void>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{interaction,responderOf}from"#channel/interaction-prompts.js";import{sessionTables}from"#protocol/session-projection/session-view.js";import{slackSessionTargetOf}from"#public/channels/slack/api.js";import{slackUserIdForPrincipal,slackUserIdFromAuthContext}from"#public/channels/slack/auth.js";import{SLACK_MAX_BLOCKS_PER_MESSAGE,truncateMessageText}from"#public/channels/slack/limits.js";import{isRefusedPayloadCode,refusedPayloadError}from"#public/channels/slack/reply-delivery.js";import{compactDefault,endingOf,renderRequestCard,sameJson}from"#public/channels/slack/request-card-render.js";import{deliverPrivateInteraction}from"#public/channels/slack/private-approval-delivery.js";const log=createLogger(`slack.request-cards`);async function postRequestCards(e,t,n,r){forgetSettledCards(t);let i=[],a=[];for(let n of e.requests)((r===void 0?`thread`:await r(n,t))===`direct-message`?i:a).push(n);await postThreadCards(a,t,n);for(let e of i)await postDirectMessageCard(e,t,n)}async function postThreadCards(e,t,n){let{channel:r}=t,i=r.state.channelId;if(e.length===0)return;if(i===null){log.warn(`thread input requests not delivered because the session has no Slack channel`,{requestIds:e.map(e=>e.interactionId),sessionId:t.session.id});return}let a=r.slack.codeChannel?slackSessionTargetOf(r.slack):void 0,o=e.map(e=>({interactionId:e.interactionId,...renderRequestCard(n,cardOf(e,`thread`,t),a)})),post=async e=>(await r.thread.post({blocks:[...e.blocks],text:e.text})).id,s=new Map;await postGroups(o,`details`,post,(e,t)=>{for(let n of e)s.set(n.interactionId,t)}),await postGroups(o,`controls`,post,(e,t)=>{let n={channelId:i,destination:`thread`,requests:e.map(({interactionId:e,message:t})=>cardRequest(e,t.blocks,s.get(e))),ts:t};a!==void 0&&(n.route=a),remember(r.state,n)})}async function postGroups(e,t,n,r){let send=async e=>{let t=await n(joinParts(e));t&&r(e,t)};for(let n of groupParts(e,e=>e.parts[t]))try{await send(n)}catch(r){let i=e.filter(e=>n.some(t=>t.interactionId===e.interactionId)),a=i.every(e=>sameJson(e.parts[t],e.fallback[t]));if(refusedPayloadError(r)===void 0||a)throw r;logError(log,`Slack refused rendered input requests; posting eve's defaults`,r);for(let e of groupParts(i,e=>e.fallback[t]))await send(e)}}function groupParts(e,t){let n=[],r=0;for(let i of e){let e=t(i);if(e===void 0)continue;let a=n.at(-1);a&&r+e.blocks.length<=SLACK_MAX_BLOCKS_PER_MESSAGE?(a.push({interactionId:i.interactionId,message:e}),r+=e.blocks.length):(n.push([{interactionId:i.interactionId,message:e}]),r=e.blocks.length)}return n}function joinParts(e){return{blocks:e.flatMap(e=>e.message.blocks),text:truncateMessageText(e.map(e=>e.message.text).join(`
|
|
2
|
+
`))}}async function postDirectMessageCard(e,t,n){let{channel:r}=t,i=slackUserIdFromAuthContext(t.session.auth.current)??r.state.triggeringUserId;if(!i){log.warn(`direct-message input request not delivered because no reviewer was resolved`,{requestId:e.interactionId,sessionId:t.session.id});return}let a=slackSessionTargetOf(r.slack),o=await deliverPrivateInteraction({card:renderRequestCard(n,cardOf(e,`direct-message`,t),a),previewMessageTs:r.state.triggeringMessageTs??r.slack.threadTs,reviewer:i,slack:r.slack}),s={channelId:o.channelId,destination:`direct-message`,requests:[cardRequest(e.interactionId,o.blocks,o.detailsTs)],route:a,ts:o.ts};remember(r.state,s);try{let t=await r.thread.post(`Waiting on ${e.request.kind===`approval`?`approval`:`a response`} from <@${i}>…`);t.id&&replace(r.state,s,{...s,announcementTs:t.id})}catch(n){logError(log,`failed to announce private input request`,n,{requestId:e.interactionId,sessionId:t.session.id})}}async function showAnswerChange(e,t,n){forgetSettledCards(t);let r=recordOf(t.channel.state,e);r!==void 0&&sessionTables(t.view).interactions[e]?.status===`open`&&replace(t.channel.state,r,await writeRequests(t,n,r,[e]))}async function settleRequestCard(e,t,n){let{channel:r}=t;forgetSettledCards(t);let i=recordOf(r.state,e),a=sessionTables(t.view),o=(i?.requests??[]).filter(e=>e.settled!==!0&&a.interactions[e.interactionId]?.status===`settled`);if(i===void 0||o.length===0)return;let s=o.map(e=>e.interactionId),c=await writeRequests(t,n,i,s),l={...c,requests:c.requests.map(e=>s.includes(e.interactionId)?{...e,settled:!0}:e)};if(replace(r.state,i,l),l.announcementTs!==void 0&&r.state.channelId!==null)for(let e of s){let n=interaction(a,e);if(n===void 0)continue;let i=cardOf(n,l.destination,t),o=i.answeredBy===void 0?``:` by <@${i.answeredBy}>`;await callSlack(r,`chat.update`,e,{channel:r.state.channelId,text:`${endingOf(i.interaction).text}${o}.`,ts:l.announcementTs})}for(let e of o)e.detailsTs!==void 0&&await retireDetails(r,l,e.detailsTs);l.requests.every(e=>e.settled===!0)&&replace(r.state,l,void 0)}async function writeRequests(e,t,n,r){let i=sessionTables(e.view),a=r.flatMap(r=>{let a=interaction(i,r);if(a===void 0)return[];let o=renderRequestCard(t,cardOf(a,n.destination,e),n.route),s=o.fallback.controls;return[{compact:compactDefault(s),fallback:s,interactionId:r,rendered:o.parts.controls}]}),blocksOf=e=>e.flatMap(e=>e.blocks),[o,...s]=[`rendered`,`fallback`,`compact`].map(e=>{let t=[];return{kind:e,requests:n.requests.map(n=>{let r=a.find(e=>e.interactionId===n.interactionId);return r===void 0?n:(t.push(r[e].text),{...n,blocks:r[e].blocks})}),text:truncateMessageText(t.join(`
|
|
3
|
+
`))}}).filter(e=>blocksOf(e.requests).length<=SLACK_MAX_BLOCKS_PER_MESSAGE);if(o===void 0||(o.kind!==`rendered`&&log.warn(`input request card would pass Slack's 50 blocks; using eve's default`,{fallback:o.kind,requestIds:r}),sameJson(blocksOf(o.requests),blocksOf(n.requests))))return n;let update=async t=>await callSlack(e.channel,`chat.update`,r.join(`,`),{blocks:blocksOf(t.requests),channel:n.channelId,text:t.text,ts:n.ts}),c=await update(o);if(c===`ok`)return{...n,requests:o.requests};let l=s.find(e=>!sameJson(blocksOf(e.requests),blocksOf(o.requests)));return c!==`refused`||l===void 0||sameJson(blocksOf(l.requests),blocksOf(n.requests))?n:await update(l)===`ok`?{...n,requests:l.requests}:n}async function retireDetails(e,t,n){let r=e.state.interactionCards??[],i=r.flatMap(e=>e.requests.filter(e=>e.detailsTs===n));i.length===0||i.some(e=>e.settled!==!0)||(await callSlack(e,`chat.delete`,n,{channel:t.channelId,ts:n}),e.state.interactionCards=r.map(e=>({...e,requests:e.requests.map(e=>{if(e.detailsTs!==n)return e;let{detailsTs:t,...r}=e;return r})})))}async function callSlack(e,t,n,r){try{let i=await e.slack.request(t,r);return i.ok===!0?`ok`:(log.warn(`failed to update input request message`,{error:i.error,operation:t,requestId:n}),isRefusedPayloadCode(i.error)?`refused`:`failed`)}catch(e){return logError(log,`failed to update input request message`,e,{operation:t,requestId:n}),`failed`}}function cardOf(e,t,n){let r=sessionTables(n.view),i=standingOf(r,e),a={audience:t===`direct-message`?`private`:normalizeChannelAudience(n.channel.state.audience),destination:t,interaction:i.interaction,view:n.view},o=i.responseId===void 0?void 0:slackUserIdForPrincipal(n.channel.state,responderOf(r,i.responseId)?.id);return o!==void 0&&(a.answeredBy=o),a}function cardRequest(e,t,n){let r={blocks:t,interactionId:e};return n!==void 0&&(r.detailsTs=n),r}function standingOf(e,t){let{interactionId:n}=t;if(t.status===`settled`){let r=e.interactions[n]?.cause;return{interaction:t,responseId:t.answer!==void 0&&r!==void 0&&`responseId`in r?r.responseId:void 0}}let{answer:r,...i}=t,a=Object.values(e.responses).findLast(e=>e.interactionId===n&&e.status===`admitted`&&e.value!==void 0);return a?.value===void 0?{interaction:i,responseId:void 0}:{interaction:{...i,answer:{...a.value,interactionId:n}},responseId:a.responseId}}function forgetSettledCards(e){let{state:t}=e.channel,n=t.interactionCards;if(n===void 0||n.length===0)return;let r=sessionTables(e.view),i=e.position.line,a=n.filter(e=>e.requests.some(e=>{let t=r.interactions[e.interactionId];return t!==void 0&&(t.status===`open`||(t.closedAt??i)>=i)}));a.length!==n.length&&(t.interactionCards=a)}function recordOf(e,t){return e.interactionCards?.find(e=>e.requests.some(e=>e.interactionId===t))}function remember(e,t){e.interactionCards=[...e.interactionCards??[],t]}function replace(e,t,n){e.interactionCards=(e.interactionCards??[]).flatMap(e=>e.ts!==t.ts||e.channelId!==t.channelId?[e]:n===void 0?[]:[n])}async function notifyRefusedResponder(e,t){let n=slackUserIdForPrincipal(t.state,e.responder?.id);n!==void 0&&await t.thread.postEphemeral(n,e.reason??`We couldn’t verify your response. Please try again.`)}export{notifyRefusedResponder,postRequestCards,settleRequestCard,showAnswerChange};
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { ChannelFrom } from "#channel/channel-operations.js";
|
|
2
|
-
import type { SlackChannelState } from "#public/channels/slack/slackChannel.js";
|
|
2
|
+
import type { SlackChannelState, SlackSessionTarget } from "#public/channels/slack/slackChannel.js";
|
|
3
3
|
/** `action_id` of the sign-in prompt's Cancel button; outside eve's `eve_input:` HITL prefix. */
|
|
4
4
|
export declare const SIGN_IN_CANCEL_ACTION_ID = "eve_sign_in:cancel";
|
|
5
5
|
/** The held turn a sign-in prompt's Cancel button cancels. */
|
|
6
|
-
export
|
|
7
|
-
readonly channelId: string;
|
|
8
|
-
readonly threadTs: string;
|
|
6
|
+
export type SignInCancelTarget = SlackSessionTarget & {
|
|
9
7
|
readonly turnId: string;
|
|
10
|
-
}
|
|
8
|
+
};
|
|
11
9
|
/** Reads a Cancel click's target from a sign-in prompt's button value. */
|
|
12
10
|
export declare function parseSignInCancelTarget(value: unknown): SignInCancelTarget | undefined;
|
|
13
11
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"#internal/logging.js";import{
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{slackSessionToken}from"#public/channels/slack/api.js";const log=createLogger(`slack.interactions`),SIGN_IN_CANCEL_ACTION_ID=`eve_sign_in:cancel`;function parseSignInCancelTarget(e){if(typeof e!=`string`)return;let t;try{t=JSON.parse(e)}catch{return}if(typeof t!=`object`||!t)return;let{channelId:n,codeChannel:r,threadTs:i,turnId:a}=t;if(typeof n==`string`&&typeof a==`string`)return r===!0?{channelId:n,codeChannel:r,turnId:a}:typeof i==`string`?{channelId:n,threadTs:i,turnId:a}:void 0}function dispatchSignInCancel(e,t){let n=readSignInCancel(e);return n!==void 0&&(t.waitUntil(cancelHeldSignIn(n,t)),!0)}function readSignInCancel(e){let t=parseSignInCancelTarget((Array.isArray(e.actions)?e.actions:[]).find(e=>isObjectRecord(e)&&e.action_id===`eve_sign_in:cancel`)?.value);if(t!==void 0)return{responseUrl:typeof e.response_url==`string`?e.response_url:void 0,target:t}}async function cancelHeldSignIn(e,t){let{turnId:n}=e.target;try{await t.from(slackSessionToken(e.target)).cancel({turnId:n})}catch(e){log.error(`sign-in cancel delivery failed`,{error:e});return}if(e.responseUrl!==void 0)try{await fetch(e.responseUrl,{body:JSON.stringify({delete_original:!0}),headers:{"content-type":`application/json`},method:`POST`})}catch(e){log.warn(`failed to remove the cancelled sign-in prompt`,{error:e})}}function isObjectRecord(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}export{SIGN_IN_CANCEL_ACTION_ID,dispatchSignInCancel,parseSignInCancelTarget};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ChannelEventContext, ChannelEventOf, CommittedLine } from "#public/definitions/channel.js";
|
|
2
2
|
import type { SessionEvent } from "#protocol/session-event.js";
|
|
3
3
|
import type { MessageContent } from "#shared/message-content.js";
|
|
4
|
+
import { type SessionSendOptions } from "#channel/session.js";
|
|
4
5
|
import type { ChannelResolveSession, ChannelSource } from "#channel/channel-operations.js";
|
|
5
6
|
import type { Delivery } from "#shared/delivery.js";
|
|
6
7
|
import type { SessionAuthContext, TurnPolicy } from "#channel/types.js";
|
|
7
8
|
import type { VercelConnectMetadata } from "#shared/vercel-connect-metadata.js";
|
|
8
9
|
import type { CardElement } from "#public/channels/slack/cards.js";
|
|
10
|
+
import type { BlockKitBlock } from "#public/channels/slack/blocks.js";
|
|
11
|
+
import type { SlackHitlRoute } from "#public/channels/slack/hitl.js";
|
|
9
12
|
import type { SessionContext } from "#public/definitions/callback-context.js";
|
|
10
13
|
import type { SessionHistoryMessage } from "#shared/session-history.js";
|
|
11
14
|
import type { ChannelContinuationOps } from "#public/definitions/channel.js";
|
|
@@ -83,28 +86,57 @@ export interface SlackAuthorizationEventContext {
|
|
|
83
86
|
*/
|
|
84
87
|
readonly state: SlackChannelState;
|
|
85
88
|
}
|
|
89
|
+
/** One request's controls in a posted card message. */
|
|
90
|
+
export interface SlackInteractionCardRequest {
|
|
91
|
+
readonly interactionId: string;
|
|
92
|
+
/** The controls blocks the message shows for this request now. */
|
|
93
|
+
readonly blocks: readonly BlockKitBlock[];
|
|
94
|
+
/** The separate details message, deleted once every request it covers settled. */
|
|
95
|
+
readonly detailsTs?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Set once eve sent the request's settled rendering, even if Slack refused it, so a failed
|
|
98
|
+
* update is never retried forever. The message is forgotten once every request has it.
|
|
99
|
+
*/
|
|
100
|
+
readonly settled?: true;
|
|
101
|
+
}
|
|
102
|
+
/** A message eve posted holding the controls of one or more input requests. */
|
|
103
|
+
export interface SlackInteractionCardRecord {
|
|
104
|
+
/** The message's conversation: the session's channel, or the reviewer's DM. */
|
|
105
|
+
readonly channelId: string;
|
|
106
|
+
readonly ts: string;
|
|
107
|
+
readonly destination: SlackApprovalChannel;
|
|
108
|
+
/** The session a card's controls answer back to: a direct message's thread, or a code channel. */
|
|
109
|
+
readonly route?: SlackHitlRoute;
|
|
110
|
+
/** Thread post announcing a direct-message request; updated with the outcome. */
|
|
111
|
+
readonly announcementTs?: string;
|
|
112
|
+
readonly requests: readonly SlackInteractionCardRequest[];
|
|
113
|
+
}
|
|
86
114
|
/**
|
|
87
115
|
* JSON-serializable per-session state, stored verbatim across workflow
|
|
88
116
|
* step boundaries. Anything written here must round-trip through
|
|
89
117
|
* `JSON.stringify` / `JSON.parse`.
|
|
90
118
|
*/
|
|
91
|
-
export interface SlackPendingApprovalCard {
|
|
92
|
-
/** Thread post announcing a direct-message approval; updated with the outcome. */
|
|
93
|
-
readonly announcementTs?: string;
|
|
94
|
-
/** Separate tool-input details post; deleted once none of its approvals are pending. */
|
|
95
|
-
readonly detailsMessageTs?: string;
|
|
96
|
-
/** Channel containing the approval card; differs from the session channel for DM delivery. */
|
|
97
|
-
readonly messageChannelId?: string;
|
|
98
|
-
readonly messageBlocks: readonly unknown[];
|
|
99
|
-
readonly messageTs: string;
|
|
100
|
-
}
|
|
101
119
|
export interface SlackChannelState extends UndeliveredReplyState {
|
|
102
120
|
/** Audience classification captured before the session is dispatched. */
|
|
103
121
|
audience?: ChannelAudience;
|
|
104
122
|
/** Slack channel id seeded by the inbound mention. */
|
|
105
123
|
channelId: string | null;
|
|
106
|
-
/** Slack thread root ts. */
|
|
124
|
+
/** Slack thread root ts; `null` in a code channel, which is one session across its threads. */
|
|
107
125
|
threadTs: string | null;
|
|
126
|
+
/** Whether this session serves a Slack code channel. */
|
|
127
|
+
codeChannel?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* In a code channel, the thread of the message the current turn answers;
|
|
130
|
+
* `null` posts top-level.
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
replyThreadTs?: string | null;
|
|
134
|
+
/**
|
|
135
|
+
* In a code channel, the thread of the latest delivered message, which the
|
|
136
|
+
* next turn replies in.
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
pendingReplyThreadTs?: string | null;
|
|
108
140
|
/** Slack team id, when the inbound event carried one. */
|
|
109
141
|
teamId: string | null;
|
|
110
142
|
/** Slack message ts that triggered the active turn. */
|
|
@@ -137,7 +169,13 @@ export interface SlackChannelState extends UndeliveredReplyState {
|
|
|
137
169
|
* resolution outcome.
|
|
138
170
|
*/
|
|
139
171
|
pendingAuthMessageTs?: Record<string, string>;
|
|
140
|
-
|
|
172
|
+
/**
|
|
173
|
+
* The messages holding open or recently answered approvals, questions, and budget prompts.
|
|
174
|
+
* Each is updated as its requests change and forgotten once all of them settle. eve ignores
|
|
175
|
+
* the `pendingApprovalCards` field that earlier versions wrote, so cards posted before an
|
|
176
|
+
* upgrade keep their buttons.
|
|
177
|
+
*/
|
|
178
|
+
interactionCards?: readonly SlackInteractionCardRecord[];
|
|
141
179
|
/**
|
|
142
180
|
* Each recent turn's tool calls and the task card eve last wrote for it, by
|
|
143
181
|
* turn id. The default `turn.paused` handler names the working tasks.
|
|
@@ -177,6 +215,8 @@ export interface SlackInstrumentationMetadata extends Record<string, unknown> {
|
|
|
177
215
|
readonly teamId: string | null;
|
|
178
216
|
readonly threadTs: string | null;
|
|
179
217
|
readonly triggeringUserId: string | null;
|
|
218
|
+
/** `true` in a code channel session; absent elsewhere. */
|
|
219
|
+
readonly codeChannel?: true;
|
|
180
220
|
}
|
|
181
221
|
/**
|
|
182
222
|
* Slack channel credentials: outbound bot token plus inbound webhook
|
|
@@ -224,6 +264,24 @@ export interface SlackReceiveTarget {
|
|
|
224
264
|
* exclusive with {@link threadTs}.
|
|
225
265
|
*/
|
|
226
266
|
readonly initialMessage?: SlackInitialMessage;
|
|
267
|
+
/**
|
|
268
|
+
* Opens a Slack code channel from a message in {@link channelId} and starts
|
|
269
|
+
* the session there, as the code channel's one session. Slack links the
|
|
270
|
+
* channel and the origin message to each other. One requester gets one code
|
|
271
|
+
* channel per origin message, so a repeated send joins that channel's
|
|
272
|
+
* session. Requires `codeChannels: true`. Mutually exclusive with
|
|
273
|
+
* {@link threadTs} and {@link initialMessage}.
|
|
274
|
+
*/
|
|
275
|
+
readonly codeChannel?: SlackCodeChannelTarget;
|
|
276
|
+
}
|
|
277
|
+
/** The code channel `slackChannel().receive` opens for `target.codeChannel`. */
|
|
278
|
+
export interface SlackCodeChannelTarget {
|
|
279
|
+
/** Channel name shown in Slack. Omit it to let Slack name the channel from the message. */
|
|
280
|
+
readonly name?: string;
|
|
281
|
+
/** ts of the message the code channel continues. */
|
|
282
|
+
readonly originTs: string;
|
|
283
|
+
/** Thread of the origin message: its own ts when top-level. */
|
|
284
|
+
readonly originThreadTs: string;
|
|
227
285
|
}
|
|
228
286
|
/**
|
|
229
287
|
* Pre-agent post issued by `slackChannel().receive` when the caller
|
|
@@ -237,14 +295,9 @@ export interface SlackInitialMessage {
|
|
|
237
295
|
/**
|
|
238
296
|
* Options for one turn requested by a generic Slack event handler.
|
|
239
297
|
*/
|
|
240
|
-
export
|
|
241
|
-
readonly auth: SessionAuthContext | null;
|
|
242
|
-
/** Prior conversation added as user and assistant turns before the message. */
|
|
243
|
-
readonly history?: readonly SessionHistoryMessage[];
|
|
298
|
+
export type SlackEventSendOptions = SessionSendOptions & {
|
|
244
299
|
readonly target: SlackReceiveTarget;
|
|
245
|
-
|
|
246
|
-
readonly title?: string;
|
|
247
|
-
}
|
|
300
|
+
};
|
|
248
301
|
/** Options for answering pending input requests from a generic Slack event handler. */
|
|
249
302
|
export interface SlackEventRespondOptions {
|
|
250
303
|
readonly auth: SessionAuthContext | null;
|
|
@@ -258,12 +311,18 @@ export type SlackEventSendFn = (message: MessageContent, options: SlackEventSend
|
|
|
258
311
|
/** Answers pending input requests on one Slack thread. */
|
|
259
312
|
export type SlackEventRespondFn = <const TResponses extends readonly Answer[]>(answers: StrictAnswers<TResponses>, options: SlackEventRespondOptions) => ReturnType<SlackSessionOperations["respond"]>;
|
|
260
313
|
/**
|
|
261
|
-
* Slack
|
|
314
|
+
* Slack session identity used by workspace-scoped inbound helpers: a thread,
|
|
315
|
+
* or a code channel, whose one session spans all of its threads.
|
|
262
316
|
*/
|
|
263
|
-
export
|
|
317
|
+
export type SlackSessionTarget = {
|
|
264
318
|
readonly channelId: string;
|
|
265
319
|
readonly threadTs: string;
|
|
266
|
-
|
|
320
|
+
readonly codeChannel?: never;
|
|
321
|
+
} | {
|
|
322
|
+
readonly channelId: string;
|
|
323
|
+
readonly codeChannel: true;
|
|
324
|
+
readonly threadTs?: never;
|
|
325
|
+
};
|
|
267
326
|
/**
|
|
268
327
|
* Imperative surface handed to `slackChannel({ onEvent })`. Generic Events API
|
|
269
328
|
* payloads are not necessarily tied to one thread, so the context exposes a
|
|
@@ -318,6 +377,25 @@ export interface SlackInboundMessageContext extends SlackContext, SlackSessionOp
|
|
|
318
377
|
isSubscribed(): Promise<boolean>;
|
|
319
378
|
/** Returns whether the inbound event explicitly mentions this bot. */
|
|
320
379
|
isBotMentioned(): boolean;
|
|
380
|
+
/**
|
|
381
|
+
* Opens a Slack code channel from this message and returns session
|
|
382
|
+
* operations bound to the channel's one session. Slack links the channel
|
|
383
|
+
* in this thread. Send the first turn with the returned `send`, then return
|
|
384
|
+
* `null` to give the thread no turn. One author gets one code channel per
|
|
385
|
+
* message, so calling it again returns the same channel. Requires
|
|
386
|
+
* `codeChannels: true`, and throws for a message already in a code channel.
|
|
387
|
+
*/
|
|
388
|
+
openCodeChannel(options?: SlackOpenCodeChannelOptions): Promise<SlackOpenedCodeChannel>;
|
|
389
|
+
}
|
|
390
|
+
/** Options for `ctx.openCodeChannel()`. */
|
|
391
|
+
export interface SlackOpenCodeChannelOptions {
|
|
392
|
+
/** Channel name shown in Slack. Omit to let Slack name it from the message. */
|
|
393
|
+
readonly name?: string;
|
|
394
|
+
}
|
|
395
|
+
/** A code channel opened by `ctx.openCodeChannel()`, bound to its one session. */
|
|
396
|
+
export interface SlackOpenedCodeChannel extends SlackSessionOperations {
|
|
397
|
+
/** Slack channel id of the new code channel. */
|
|
398
|
+
readonly channelId: string;
|
|
321
399
|
}
|
|
322
400
|
/** Interaction-scoped context handed to `slackChannel({ onInteraction })`. */
|
|
323
401
|
export interface SlackInteractionContext extends SlackContext, SlackSessionOperations {
|
|
@@ -488,6 +566,15 @@ export interface SlackChannelConfig {
|
|
|
488
566
|
readonly route?: string;
|
|
489
567
|
/** Policy for accepted messages that arrive while a turn is active. */
|
|
490
568
|
readonly turnPolicy?: TurnPolicy;
|
|
569
|
+
/**
|
|
570
|
+
* Serves each Slack code channel as one session and answers every message in
|
|
571
|
+
* it, not just mentions. Un-mentioned messages go to `onMessage`, else
|
|
572
|
+
* `onAppMention` (where `ctx.isBotMentioned()` returns `false`), else the
|
|
573
|
+
* default handler. Adds the code channel scopes, events, and feature flag to
|
|
574
|
+
* the app manifest. Without it, eve treats a code channel like any other
|
|
575
|
+
* channel and never looks one up.
|
|
576
|
+
*/
|
|
577
|
+
readonly codeChannels?: boolean;
|
|
491
578
|
/**
|
|
492
579
|
* Inbound upload policy applied to file attachments before they reach
|
|
493
580
|
* the harness. Violating attachments are dropped with a warning so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{markEventHandled}from"./utils.js";import{attachInputText}from"#internal/input-text.js";import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{setChannelBuildMetadata}from"#channel/compiled-channel.js";import{POST,defineChannel}from"#public/definitions/channel.js";import{mergeUploadPolicy}from"#public/channels/upload-policy.js";import{maxBytesOf}from"#internal/attachments/limited-read.js";import{withUndeliveredReplyNote}from"#public/channels/undelivered-reply.js";import{buildSlackBinding,buildSlackWorkspaceHandle,fetchSlackThreadWindow,slackContinuationToken}from"#public/channels/slack/api.js";import{buildSlackAuthContext,slackUserIdFromAuthContext}from"#public/channels/slack/auth.js";import{resolveSlackTransportOptions}from"#public/channels/slack/transport.js";import{parseMessageEvent,parseSlackEventEnvelope,slackEventBotUserId,slackEventInstallationTeamId,slackEventReceivingBotUserId,slackMessageFromWebhookPayload}from"#public/channels/slack/inbound.js";import{withFinalReplyDelivery}from"#public/channels/slack/reply-delivery.js";import{defaultEvents,defaultInteractionOpenedHandler,defaultOnMessage,defaultReceived}from"#public/channels/slack/defaults.js";import{composeSlackRenderers}from"#public/channels/slack/renderers.js";import{loadThreadContextMessages}from"#public/channels/slack/thread.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{buildSlackTurnMessage,collectInboundFileParts,createSlackFetchFile}from"#public/channels/slack/attachments.js";import{renderDefaultSlackTaskCard,withTaskCards}from"#public/channels/slack/task-card.js";import{formatSlackInboundMessage,formatSlackThreadContext,slackThreadSessionHistory}from"#public/channels/slack/model-context.js";import{isPrivateSlackConversation}from"#public/channels/slack/privacy.js";import{SLACK_CHANNEL_DEFAULT_ROUTE}from"#public/channels/slack/constants.js";import{defineSlackAppManifest}from"#public/channels/slack/app-manifest.js";import{handleInteractionPost}from"#public/channels/slack/interactions.js";import{verifySlackRequest as verifySlackRequest$1}from"#public/channels/slack/verify.js";const log=createLogger(`slack.channel`);function rebuildSlackContext(e,t,n,r){let{thread:i,slack:a}=buildSlackBinding({api:n,botToken:r?.botToken,channelId:e.channelId??``,threadTs:e.threadTs??``,installationTeamId:e.installationTeamId??void 0,teamId:e.teamId??void 0,onThreadTsChanged(n){e.threadTs=n,e.channelId&&t.continuation?.alias(slackContinuationToken({channelId:e.channelId,threadTs:n}))},onPosted(){e.postedInThread=!0}});return{thread:i,slack:a,state:e}}function slackChannel(e={}){let t=resolveSlackTransportOptions(e.api),n=mergeUploadPolicy(e.uploadPolicy),r=createSlackFetchFile({api:t,botToken:e.credentials?.botToken,maxBytes:maxBytesOf(n)}),i=e.onAnswers??defaultOnAnswers,a=composeSlackRenderers(e.renderers??[],{events:{...defaultEvents,"interaction.opened":defaultInteractionOpenedHandler(e.approvalChannel)},received:defaultReceived,taskCard:renderDefaultSlackTaskCard}),o=a.events[`turn.started`],s=withTaskCards({...a.events,"content.completed":withFinalReplyDelivery(a.events[`content.completed`]),async"turn.started"(e,t){let{channel:n}=t,r=slackUserIdFromAuthContext(t.session.auth.current);r!==void 0&&(n.state.triggeringUserId=r),await o?.(e,t)}},a.taskCard),c=new Set,l=defineChannel({turnPolicy:e.turnPolicy,uploadPolicy:e.uploadPolicy,state:{channelId:null,threadTs:null,teamId:null,installationTeamId:null,triggeringUserId:null,triggeringMessageTs:null,pendingToolCallMessage:null,pendingAuthMessageTs:{},pendingApprovalCards:{},slackUsersByPrincipal:{}},fetchFile:r,metadata(e){return{channelId:e.channelId,teamId:e.teamId,threadTs:e.threadTs,triggeringUserId:e.triggeringUserId??null}},audience:({state:e})=>e.audience??`unknown`,context(n,r){return rebuildSlackContext(n,r,t,e.credentials)},deliver(e,t){return recordSlackPrincipal(t.state,t.session.auth.current,e),withUndeliveredReplyNote(t.state,e,`Slack`)},routes:[POST(e.route??SLACK_CHANNEL_DEFAULT_ROUTE,async(r,o)=>{let{waitUntil:s}=o,{from:l,resolveSession:u}=o,d=await verifyInbound(r,e.credentials);return d===null?new Response(`unauthorized`,{status:401}):shouldDropSlackHttpTimeoutRetry(r.headers)?new Response(`ok`):(r.headers.get(`content-type`)??``).includes(`application/x-www-form-urlencoded`)?handleInteractionPost(d,{from:l,resolveSession:u,waitUntil:s},{api:t,config:e,onAnswers:i}):handleEventPost({api:t,body:d,from:l,resolveSession:u,waitUntil:s,config:e,received:a.received,uploadPolicy:n,handledEvents:c,headers:r.headers})})],receive(n,{from:r}){return receiveOnSlack(n,{from:r,api:t,credentials:e.credentials})},events:s}),u=e.credentials,d=defineSlackAppManifest({botName:e.botName});return setChannelBuildMetadata(l,e=>({externalCredentials:u?.vercelConnect,manifest:d.build(e)})),l}function defaultOnAnswers(e){return{auth:e.defaultAuth}}async function receiveOnSlack(e,t){let n=e.target,r=n.channelId;if(!r||typeof r!=`string`)throw Error(`slackChannel().receive requires target.channelId.`);let i=typeof n.threadTs==`string`?n.threadTs:``,a=typeof n.installationTeamId==`string`?n.installationTeamId:t.installationTeamId,o=n.initialMessage;if(o&&i.length>0)throw Error("slackChannel().receive: `threadTs` and `initialMessage` are mutually exclusive.");let s=i;if(o){let{thread:e}=buildSlackBinding({api:t.api,botToken:t.credentials?.botToken,channelId:r,threadTs:``,installationTeamId:a,teamId:t.teamId}),n={card:o.card};o.fallbackText!==void 0&&(n.fallbackText=o.fallbackText),s=(await e.post(n)).id}let c=s||crypto.randomUUID(),l=n.audience===void 0?void 0:normalizeChannelAudience(n.audience),u={channelId:r,installationTeamId:a??null,threadTs:s||null,teamId:t.teamId??null,triggeringUserId:t.triggeringUserId??null};return l!==void 0&&(u.audience=l),t.from(slackContinuationToken({channelId:r,threadTs:c})).send(e.message,{auth:e.auth,history:e.history,state:u,title:e.title})}function shouldDropSlackHttpTimeoutRetry(e){return Number(e.get(`x-slack-retry-num`)??`0`)>=1&&e.get(`x-slack-retry-reason`)===`http_timeout`}async function handleEventPost(e){let{config:t}=e,n,r;try{n=parseSlackWebhookBody(e.body,{headers:e.headers}),r=parseSlackEventEnvelope(e.body)}catch(e){return log.warn(`inbound webhook body is not valid JSON`,{error:e}),new Response(`ok`)}if(n.kind===`url_verification`)return new Response(n.challenge,{status:200,headers:{"content-type":`text/plain`}});if(r===null)return new Response(`ok`);let i=typeof r.api_app_id==`string`?r.api_app_id:void 0,a=slackEventBotUserId(r),o=slackEventReceivingBotUserId(r),s=slackEventInstallationTeamId(r),dispatch=null,c=null,dispatchMessage=(n,r,c)=>()=>dispatchSlackMessage({api:e.api,appId:i,botUserId:a,receivingBotUserId:o,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,handler:c,installationTeamId:s,kind:n,message:r,received:e.received,resolvesTypedInput:t.onAnswers===void 0&&t.approvalChannel===void 0,threadContext:t.threadContext,uploadPolicy:e.uploadPolicy});if(n.kind===`app_mention`||n.kind===`direct_message`){let e=n.kind,r=slackMessageFromWebhookPayload(n,s);if(r!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},r)){let n=(e===`app_mention`?t.onAppMention:t.onDirectMessage)??t.onMessage;n===void 0?c=dispatchMessage(e,r,defaultOnMessage):dispatch=dispatchMessage(e,r,n)}}let l=t.onMessage;if(dispatch===null&&l!==void 0){let e=parseMessageEvent(r);e!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},e)&&(a===void 0||!e.text.includes(`<@${a}`))&&(dispatch=dispatchMessage(`channel_message`,e,l))}let u=t.onEvent;if(dispatch===null&&u!==void 0&&(dispatch=()=>dispatchSlackEvent({api:e.api,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,envelope:r,handler:u,installationTeamId:s})),dispatch??=c,dispatch===null)return new Response(`ok`);let d=r.event_id;if(d){if(e.handledEvents.has(d))return log.warn(`received a duplicate event`,{event_id:d,event_time:r.event_time,retry_num:n.retry?.num??`(null)`,retry_reason:n.retry?.reason??`(null)`}),new Response(`ok`);markEventHandled(d,e.handledEvents)}return e.waitUntil(dispatch()),new Response(`ok`)}function isSelfAuthoredSlackMessage(e,t){return e.botUserId!==void 0&&t.author?.userId===e.botUserId||e.appId!==void 0&&t.raw.app_id===e.appId}async function dispatchSlackMessage(e){let t=slackContinuationToken({channelId:e.message.channelId,threadTs:e.message.threadTs}),{thread:n,slack:r}=buildSlackBinding({api:e.api,appId:e.appId,botToken:e.credentials?.botToken,botUserId:e.botUserId,channelId:e.message.channelId,threadTs:e.message.threadTs,threadHasReplies:e.message.threadTs!==e.message.ts,installationTeamId:e.installationTeamId,teamId:e.message.teamId}),i=e.message.author,a={channelId:e.message.channelId,installationTeamId:e.installationTeamId??null,teamId:e.message.teamId??null,threadTs:e.message.threadTs,triggeringMessageTs:e.message.ts,triggeringUserId:i?.userId??null},o=bindSlackSessionOperations({address:t,defaultAuth:i===void 0?null:buildSlackAuthContext({channelId:e.message.channelId,fullName:i.fullName,installationTeamId:e.installationTeamId,isBot:i.isBot,teamId:e.message.teamId,threadTs:e.message.threadTs,userId:i.userId,userName:i.userName}),from:e.from,resolveSession:e.resolveSession,state:a}),s,isDMOrPrivateChannel=()=>s??=isPrivateSlackConversation({channelId:e.message.channelId,raw:e.message.raw,request:r.request}),c=e.kind===`app_mention`||e.botUserId!==void 0&&e.message.text.includes(`<@${e.botUserId}`),l={...o,isBotMentioned:()=>c,isDMOrPrivateChannel,isSubscribed:async()=>await o.resolveSession()!==void 0,slack:r,thread:n},u=e.kind===`channel_message`?void 0:acknowledgeMessage(e.received,e.message,{slack:r,thread:n}),d;try{d=await e.handler(l,e.message)}catch(t){logError(log,`${e.kind} handler failed`,t,{channelId:e.message.channelId})}if(d==null){u!==void 0&&(await u,await n.startTyping());return}await(u??acknowledgeMessage(e.received,e.message,{slack:r,thread:n}));let f=await isDMOrPrivateChannel();a.audience=e.kind===`direct_message`||f?`private`:`public`,await deliverSlackMessage({botUserId:e.receivingBotUserId,credentials:e.credentials,kind:e.kind,isPrivateConversation:f,isMentioned:c,message:e.message,resolvesTypedInput:e.resolvesTypedInput,result:d,sessionOperations:o,thread:n,threadContext:e.threadContext,uploadPolicy:e.uploadPolicy})}async function acknowledgeMessage(e,t,n){try{await e(t,n)}catch(e){logError(log,`received renderer failed`,e,{channelId:t.channelId})}}async function dispatchSlackEvent(e){let t=e.envelope.event.team_id,n=typeof t==`string`?t:typeof e.envelope.team_id==`string`?e.envelope.team_id:void 0,r=[],sourceFor=t=>e.from(slackContinuationToken({channelId:t.channelId,threadTs:t.threadTs})),i={cancel:({target:e,turnId:t})=>sourceFor(e).cancel({turnId:t}),clear:({target:e})=>sourceFor(e).clear(),compact:({target:e})=>sourceFor(e).compact(),envelope:e.envelope,reset:({reason:e,target:t})=>sourceFor(t).reset({reason:e}),resolveSession:({target:t})=>e.resolveSession(slackContinuationToken({channelId:t.channelId,threadTs:t.threadTs})),respond:(t,{auth:n,target:r})=>sourceFor(r).respond(t,{auth:n,state:withSlackResponder(void 0,typeof e.envelope.event.user==`string`?e.envelope.event.user:void 0)}),send:(t,{auth:r,history:i,target:a,title:o})=>receiveOnSlack({auth:r,history:i,message:t,target:a,title:o},{from:e.from,api:e.api,credentials:e.credentials,installationTeamId:e.installationTeamId,teamId:n,...typeof e.envelope.event.user==`string`?{triggeringUserId:e.envelope.event.user}:{}}),slack:buildSlackWorkspaceHandle({api:e.api,botToken:e.credentials?.botToken,installationTeamId:e.installationTeamId,teamId:n}),waitUntil(e){r.push(e)}};try{await e.handler(i,e.envelope.event)}catch(t){logError(log,`event handler failed`,t,{eventId:e.envelope.event_id,eventType:e.envelope.event.type})}await Promise.allSettled(r)}async function verifyInbound(e,t){try{return await verifySlackRequest$1(e,{signingSecret:t?.signingSecret??(t?.webhookVerifier?void 0:process.env.SLACK_SIGNING_SECRET),webhookVerifier:t?.webhookVerifier})}catch(e){return log.warn(`slack inbound verification failed`,{error:e}),null}}async function deliverSlackMessage(e){let{message:t,thread:n}=e;try{let r=e.threadContext===void 0?[]:await loadThreadContextMessages(n,t,e.threadContext),i=r.length>0&&e.result.history===void 0&&await e.sessionOperations.resolveSession()===void 0,a=i?void 0:formatSlackThreadContext(r),o=e.result.history??(i?slackThreadSessionHistory(r):void 0),s=await collectInboundFileParts({botUserId:e.botUserId,fetchThreadWindow:(e,t)=>fetchSlackThreadWindow(n,e,t),isMentioned:e.kind===`app_mention`,mention:t,thread:n,policy:e.uploadPolicy}),c={botUserId:e.botUserId,channelId:t.channelId,fullName:t.author?.fullName,isMentioned:e.isMentioned,teamId:t.teamId,threadTs:t.threadTs,userId:t.author?.userId??``,userName:t.author?.userName},l=formatSlackInboundMessage(c,t),u=buildSlackTurnMessage(a===void 0?l:`${a}\n\n${l}`,s),d=e.result.context??[],f=e.isPrivateConversation?`Private message`:e.result.title??t.markdown,p=e.resolvesTypedInput?slackTypedText(t.text,e.botUserId):``,m=attachInputText({auth:e.result.auth,title:f,...d.length>0&&{context:d},...o!==void 0&&{history:o}},s.length===0?p:void 0);await e.sessionOperations.send(u,m)}catch(n){logError(log,`${e.kind} delivery failed`,n,{channelId:t.channelId})}}function slackTypedText(e,t){return t===void 0?e:e.replace(RegExp(`<@${t}(?:\\|[^>]*)?>`,`gu`),``).trim()}function recordSlackPrincipal(e,t,n){if(t===null)return;let r=n.state?.triggeringUserId,i=slackUserIdFromAuthContext(t)??(typeof r==`string`?r:void 0);i!==void 0&&(e.slackUsersByPrincipal={...e.slackUsersByPrincipal,[t.principalId]:i})}export{slackChannel};
|
|
1
|
+
import{markEventHandled}from"./utils.js";import{createHash}from"node:crypto";import{toSessionSendOptions}from"#channel/session.js";import{attachInputText}from"#internal/input-text.js";import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{SessionNotActiveError}from"#channel/session-not-active-error.js";import{setChannelBuildMetadata}from"#channel/compiled-channel.js";import{POST,defineChannel}from"#public/definitions/channel.js";import{mergeUploadPolicy}from"#public/channels/upload-policy.js";import{maxBytesOf}from"#internal/attachments/limited-read.js";import{withUndeliveredReplyNote}from"#public/channels/undelivered-reply.js";import{buildSlackBinding,buildSlackWorkspaceHandle,fetchSlackThreadWindow,slackCodeChannelContinuationToken,slackContinuationToken,slackSessionToken}from"#public/channels/slack/api.js";import{buildSlackAuthContext,slackUserIdFromAuthContext}from"#public/channels/slack/auth.js";import{createSlackCodeChannel,isSlackCodeChannel,isSlackCodeChannelOriginContext,setSlackCodeChannelStatus}from"#public/channels/slack/code-channel.js";import{resolveSlackTransportOptions}from"#public/channels/slack/transport.js";import{parseMessageEvent,parseSlackEventEnvelope,slackEventBotUserId,slackEventInstallationTeamId,slackEventReceivingBotUserId,slackMessageFromWebhookPayload}from"#public/channels/slack/inbound.js";import{withFinalReplyDelivery}from"#public/channels/slack/reply-delivery.js";import{defaultEvents,defaultInteractionEvents,defaultOnMessage,defaultReceived}from"#public/channels/slack/defaults.js";import{composeInteractionCards,composeSlackRenderers}from"#public/channels/slack/renderers.js";import{loadThreadContextMessages}from"#public/channels/slack/thread.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{buildSlackTurnMessage,collectInboundFileParts,createSlackFetchFile}from"#public/channels/slack/attachments.js";import{renderDefaultSlackTaskCard,withTaskCards}from"#public/channels/slack/task-card.js";import{formatSlackInboundMessage,formatSlackThreadContext,slackThreadSessionHistory}from"#public/channels/slack/model-context.js";import{isPrivateSlackConversation}from"#public/channels/slack/privacy.js";import{SLACK_CHANNEL_DEFAULT_ROUTE}from"#public/channels/slack/constants.js";import{defineSlackAppManifest}from"#public/channels/slack/app-manifest.js";import{handleInteractionPost}from"#public/channels/slack/interactions.js";import{verifySlackRequest as verifySlackRequest$1}from"#public/channels/slack/verify.js";const log=createLogger(`slack.channel`);function rebuildSlackContext(e,t,n,r){let{thread:i,slack:a}=buildSlackBinding({api:n,botToken:r?.botToken,channelId:e.channelId??``,threadTs:e.threadTs??``,codeChannel:e.codeChannel===!0,replyThreadTs:()=>e.replyThreadTs??void 0,installationTeamId:e.installationTeamId??void 0,teamId:e.teamId??void 0,onThreadTsChanged(n){e.threadTs=n,e.channelId&&t.continuation?.alias(slackContinuationToken({channelId:e.channelId,threadTs:n}))},onPosted(){e.postedInThread=!0}});return{thread:i,slack:a,state:e}}function slackChannel(e={}){let t=resolveSlackTransportOptions(e.api),n=mergeUploadPolicy(e.uploadPolicy),r=createSlackFetchFile({api:t,botToken:e.credentials?.botToken,maxBytes:maxBytesOf(n)}),i=e.onAnswers??defaultOnAnswers,a=composeInteractionCards(e.renderers??[]),o=composeSlackRenderers(e.renderers??[],{events:{...defaultEvents,...defaultInteractionEvents(e.approvalChannel,a)},received:defaultReceived,taskCard:renderDefaultSlackTaskCard}),s=o.events[`turn.started`],c=withTaskCards({...o.events,"content.completed":withFinalReplyDelivery(o.events[`content.completed`]),async"turn.started"(e,t){let{channel:n}=t,r=slackUserIdFromAuthContext(t.session.auth.current);r!==void 0&&(n.state.triggeringUserId=r),n.state.pendingReplyThreadTs!==void 0&&(n.state.replyThreadTs=n.state.pendingReplyThreadTs,n.state.pendingReplyThreadTs=void 0),await s?.(e,t)}},o.taskCard),l=new Set,u=defineChannel({turnPolicy:e.turnPolicy,uploadPolicy:e.uploadPolicy,state:{channelId:null,threadTs:null,teamId:null,installationTeamId:null,triggeringUserId:null,triggeringMessageTs:null,pendingToolCallMessage:null,pendingAuthMessageTs:{},interactionCards:[],slackUsersByPrincipal:{}},fetchFile:r,metadata(e){return{channelId:e.channelId,teamId:e.teamId,threadTs:e.threadTs,triggeringUserId:e.triggeringUserId??null,installationTeamId:e.installationTeamId??null,triggeringMessageTs:e.triggeringMessageTs??null,...e.codeChannel===!0&&{codeChannel:!0}}},audience:({state:e})=>e.audience??`unknown`,context(n,r){return rebuildSlackContext(n,r,t,e.credentials)},deliver(e,t){return recordSlackPrincipal(t.state,t.session.auth.current,e),followReplyThread(t.state,e),followTriggeringMessage(t.state,e),withUndeliveredReplyNote(t.state,e,`Slack`)},routes:[POST(e.route??SLACK_CHANNEL_DEFAULT_ROUTE,async(r,a)=>{let{waitUntil:s}=a,{from:c,resolveSession:u}=a,d=await verifyInbound(r,e.credentials);return d===null?new Response(`unauthorized`,{status:401}):shouldDropSlackHttpTimeoutRetry(r.headers)?new Response(`ok`):(r.headers.get(`content-type`)??``).includes(`application/x-www-form-urlencoded`)?handleInteractionPost(d,{from:c,resolveSession:u,waitUntil:s},{api:t,config:e,onAnswers:i}):handleEventPost({api:t,body:d,from:c,resolveSession:u,waitUntil:s,config:e,received:o.received,uploadPolicy:n,handledEvents:l,headers:r.headers})})],receive(n,{from:r}){return receiveOnSlack(n,{from:r,api:t,codeChannels:e.codeChannels===!0,credentials:e.credentials})},events:c}),d=e.credentials,f=defineSlackAppManifest({botName:e.botName,codeChannels:e.codeChannels===!0});return setChannelBuildMetadata(u,e=>({externalCredentials:d?.vercelConnect,manifest:f.build(e)})),u}function defaultOnAnswers(e){return{auth:e.defaultAuth}}async function receiveOnSlack(e,t){let{message:n,target:r,...i}=e,a=r,o=a.channelId;if(!o||typeof o!=`string`)throw Error(`slackChannel().receive requires target.channelId.`);let s=typeof a.threadTs==`string`?a.threadTs:``,c=typeof a.installationTeamId==`string`?a.installationTeamId:t.installationTeamId,l=a.initialMessage;if(l&&s.length>0)throw Error("slackChannel().receive: `threadTs` and `initialMessage` are mutually exclusive.");if(a.codeChannel!==void 0){if(!t.codeChannels)throw Error("slackChannel().receive: set `codeChannels: true` on slackChannel() to open code channels.");if(l||s.length>0)throw Error("slackChannel().receive: `codeChannel` can't be combined with `threadTs` or `initialMessage`.");return await receiveInNewCodeChannel(e,t,{codeChannel:a.codeChannel,installationTeamId:c,originChannelId:o})}let u=s;if(l){let{thread:e}=buildSlackBinding({api:t.api,botToken:t.credentials?.botToken,channelId:o,threadTs:``,installationTeamId:c,teamId:t.teamId}),n={card:l.card};l.fallbackText!==void 0&&(n.fallbackText=l.fallbackText),u=(await e.post(n)).id}let d=u||(i.idempotencyKey===void 0?crypto.randomUUID():threadlessIdentityFromKey(i.idempotencyKey)),f=a.audience===void 0?void 0:normalizeChannelAudience(a.audience),p={channelId:o,installationTeamId:c??null,threadTs:u||null,teamId:t.teamId??null,triggeringUserId:t.triggeringUserId??null};return f!==void 0&&(p.audience=f),t.from(slackContinuationToken({channelId:o,threadTs:d})).send(n,{...i,state:p})}function threadlessIdentityFromKey(e){return`key-${createHash(`sha256`).update(e).digest(`hex`).slice(0,32)}`}async function receiveInNewCodeChannel(e,t,n){let{codeChannel:r,installationTeamId:i,originChannelId:a}=n,{request:o}=buildSlackWorkspaceHandle({api:t.api,botToken:t.credentials?.botToken,installationTeamId:i,teamId:t.teamId}),s=await openSlackCodeChannel({auth:e.auth,installationTeamId:i,name:r.name,originChannelId:a,originTs:r.originTs,request:o,teamId:t.teamId,triggeringUserId:t.triggeringUserId??slackUserIdFromAuthContext(e.auth)}),{message:c,target:l,...u}=e;return await t.from(s.address).send(c,{...u,auth:s.auth,state:s.state})}async function openSlackCodeChannel(e){let{channelId:t,isPrivate:n}=await createSlackCodeChannel({name:e.name??``,originChannelId:e.originChannelId,originTs:e.originTs,requester:e.auth?.principalId??`anonymous`,request:e.request});return{address:slackCodeChannelContinuationToken(t),auth:codeChannelAuth(e.auth,t),channelId:t,state:{audience:n?`private`:`public`,channelId:t,codeChannel:!0,installationTeamId:e.installationTeamId??null,replyThreadTs:null,teamId:e.teamId??null,threadTs:null,triggeringUserId:e.triggeringUserId??null}}}function codeChannelAuth(e,t){if(e?.authenticator!==`slack-webhook`)return e;let{thread_ts:n,...r}=e.attributes;return{...e,attributes:{...r,channel_id:t}}}function shouldDropSlackHttpTimeoutRetry(e){return Number(e.get(`x-slack-retry-num`)??`0`)>=1&&e.get(`x-slack-retry-reason`)===`http_timeout`}async function handleEventPost(e){let{config:t}=e,n,r;try{n=parseSlackWebhookBody(e.body,{headers:e.headers}),r=parseSlackEventEnvelope(e.body)}catch(e){return log.warn(`inbound webhook body is not valid JSON`,{error:e}),new Response(`ok`)}if(n.kind===`url_verification`)return new Response(n.challenge,{status:200,headers:{"content-type":`text/plain`}});if(r===null)return new Response(`ok`);let i=typeof r.api_app_id==`string`?r.api_app_id:void 0,a=slackEventBotUserId(r),o=slackEventReceivingBotUserId(r),s=slackEventInstallationTeamId(r),c=r.event,l=c.channel;if(t.codeChannels===!0&&c.type===`agent_session_stopped`&&typeof l==`string`){if(isDuplicateEvent(r,n.retry,e.handledEvents))return new Response(`ok`);let i=buildSlackWorkspaceHandle({api:e.api,botToken:t.credentials?.botToken,installationTeamId:s,teamId:typeof r.team_id==`string`?r.team_id:void 0}).request,a=t.onEvent;return e.waitUntil((async()=>{if(c.thread_ts!==void 0&&!await isSlackCodeChannel(l,i)){if(a===void 0)return;await dispatchSlackEvent({api:e.api,from:e.from,resolveSession:e.resolveSession,codeChannels:!0,credentials:t.credentials,envelope:r,handler:a,installationTeamId:s});return}await cancelledARunningTurn(e.from,l)||await setSlackCodeChannelStatus(i,l,`active`)})().catch(e=>{logError(log,`agent_session_stopped handling failed`,e,{channelId:l})})),new Response(`ok`)}let u=null,d=null,dispatchMessage=(n,r,c,l)=>()=>dispatchSlackMessage({api:e.api,appId:i,botUserId:a,codeChannelOnly:l,detectCodeChannels:t.codeChannels===!0,receivingBotUserId:o,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,handler:c,installationTeamId:s,kind:n,message:r,received:e.received,resolvesTypedInput:t.onAnswers===void 0&&t.approvalChannel===void 0,threadContext:t.threadContext,uploadPolicy:e.uploadPolicy});if(n.kind===`app_mention`||n.kind===`direct_message`){let e=n.kind,r=slackMessageFromWebhookPayload(n,s);if(r!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},r)){let n=(e===`app_mention`?t.onAppMention:t.onDirectMessage)??t.onMessage;n===void 0?d=dispatchMessage(e,r,defaultOnMessage):u=dispatchMessage(e,r,n)}}let f=t.onMessage;if(u===null&&f!==void 0){let e=parseMessageEvent(r);e!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},e)&&(a===void 0||!e.text.includes(`<@${a}`))&&(u=dispatchMessage(`channel_message`,e,f))}let p=t.onEvent,m=p===void 0?void 0:()=>dispatchSlackEvent({api:e.api,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,codeChannels:t.codeChannels===!0,envelope:r,handler:p,installationTeamId:s});if(u===null&&d===null&&t.codeChannels===!0){let e=parseMessageEvent(r);if(e!==null&&e.author?.isBot===!1&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},e)&&!isSlackCodeChannelOriginContext(e.raw)&&(a===void 0||!e.text.includes(`<@${a}`))){let n=t.onAppMention;n===void 0?d=dispatchMessage(`channel_message`,e,defaultOnMessage,{}):u=dispatchMessage(`channel_message`,e,n,{otherwise:m})}}return u??=m??d,u===null||isDuplicateEvent(r,n.retry,e.handledEvents)||e.waitUntil(u()),new Response(`ok`)}function isDuplicateEvent(e,t,n){let r=e.event_id;return r?n.has(r)?(log.warn(`received a duplicate event`,{event_id:r,event_time:e.event_time,retry_num:t?.num??`(null)`,retry_reason:t?.reason??`(null)`}),!0):(markEventHandled(r,n),!1):!1}function isSelfAuthoredSlackMessage(e,t){return e.botUserId!==void 0&&t.author?.userId===e.botUserId||e.appId!==void 0&&t.raw.app_id===e.appId}async function dispatchSlackMessage(e){let t=e.message.author,n=buildSlackWorkspaceHandle({api:e.api,botToken:e.credentials?.botToken,installationTeamId:e.installationTeamId,teamId:e.message.teamId}).request,r,conversationRequest=(e,t)=>e===`conversations.info`?r??=n(e,t):n(e,t),i=e.detectCodeChannels&&e.kind!==`direct_message`&&await isSlackCodeChannel(e.message.channelId,conversationRequest);if(!i&&e.codeChannelOnly!==void 0){await e.codeChannelOnly.otherwise?.();return}let a=i?``:e.message.threadTs,o=e.message.threadTs!==e.message.ts,s=i&&o?e.message.threadTs:null,c=i?slackCodeChannelContinuationToken(e.message.channelId):slackContinuationToken({channelId:e.message.channelId,threadTs:a}),{thread:l,slack:u}=buildSlackBinding({api:e.api,appId:e.appId,botToken:e.credentials?.botToken,botUserId:e.botUserId,channelId:e.message.channelId,threadTs:a,codeChannel:i,replyThreadTs:()=>s??void 0,threadHasReplies:o,installationTeamId:e.installationTeamId,teamId:e.message.teamId}),d={channelId:e.message.channelId,installationTeamId:e.installationTeamId??null,teamId:e.message.teamId??null,threadTs:a||null,triggeringMessageTs:e.message.ts,triggeringUserId:t?.userId??null,...i&&{codeChannel:!0,replyThreadTs:s}},f=t===void 0?null:buildSlackAuthContext({channelId:e.message.channelId,fullName:t.fullName,installationTeamId:e.installationTeamId,isBot:t.isBot,teamId:e.message.teamId,threadTs:a,userId:t.userId,userName:t.userName}),p=bindSlackSessionOperations({address:c,defaultAuth:f,from:e.from,resolveSession:e.resolveSession,state:d}),m,isDMOrPrivateChannel=()=>m??=isPrivateSlackConversation({channelId:e.message.channelId,raw:e.message.raw,request:conversationRequest}),openCodeChannel=async(t={})=>{if(!e.detectCodeChannels)throw Error("ctx.openCodeChannel(): set `codeChannels: true` on slackChannel() to open code channels.");if(i)throw Error(`ctx.openCodeChannel(): this message is already in a code channel.`);return await openCodeChannelFromMessage({defaultAuth:f,from:e.from,installationTeamId:e.installationTeamId,message:e.message,name:t.name,request:n,resolveSession:e.resolveSession})},h=e.kind===`app_mention`||e.botUserId!==void 0&&e.message.text.includes(`<@${e.botUserId}`),g={...p,isBotMentioned:()=>h,isDMOrPrivateChannel,isSubscribed:async()=>await p.resolveSession()!==void 0,openCodeChannel,slack:u,thread:l},_=e.kind!==`channel_message`&&!i?acknowledgeMessage(e.received,e.message,{slack:u,thread:l}):void 0,v;try{v=await e.handler(g,e.message)}catch(t){logError(log,`${e.kind} handler failed`,t,{channelId:e.message.channelId})}if(v==null){_!==void 0&&(await _,await l.startTyping());return}await(_??acknowledgeMessage(e.received,e.message,{slack:u,thread:l}));let y=await isDMOrPrivateChannel();d.audience=e.kind===`direct_message`||y?`private`:`public`,await deliverSlackMessage({botUserId:e.receivingBotUserId,credentials:e.credentials,kind:e.kind,isPrivateConversation:y,isMentioned:h,message:e.message,resolvesTypedInput:e.resolvesTypedInput,result:v,sessionOperations:p,thread:l,threadContext:e.threadContext,uploadPolicy:e.uploadPolicy})}async function openCodeChannelFromMessage(e){let t=await openSlackCodeChannel({auth:e.defaultAuth,installationTeamId:e.installationTeamId,name:e.name,originChannelId:e.message.channelId,originTs:e.message.ts,request:e.request,teamId:e.message.teamId,triggeringUserId:e.message.author?.userId}),n=bindSlackSessionOperations({address:t.address,defaultAuth:t.auth,from:e.from,resolveSession:e.resolveSession,state:t.state}),moved=e=>e?.auth?{...e,auth:codeChannelAuth(e.auth,t.channelId)}:e;return{...n,channelId:t.channelId,send:(e,t)=>n.send(e,moved(t)),respond:(e,t)=>n.respond(e,moved(t))}}async function acknowledgeMessage(e,t,n){try{await e(t,n)}catch(e){logError(log,`received renderer failed`,e,{channelId:t.channelId})}}async function dispatchSlackEvent(e){let t=e.envelope.event.team_id,n=typeof t==`string`?t:typeof e.envelope.team_id==`string`?e.envelope.team_id:void 0,r=[],sourceFor=t=>e.from(slackSessionToken(t)),i={cancel:({target:e,turnId:t})=>sourceFor(e).cancel({turnId:t}),clear:({target:e})=>sourceFor(e).clear(),compact:({target:e})=>sourceFor(e).compact(),envelope:e.envelope,reset:({reason:e,target:t})=>sourceFor(t).reset({reason:e}),resolveSession:({target:t})=>e.resolveSession(slackSessionToken(t)),respond:(t,{auth:n,target:r})=>sourceFor(r).respond(t,{auth:n,state:withSlackResponder(void 0,typeof e.envelope.event.user==`string`?e.envelope.event.user:void 0)}),send:async(t,r)=>await receiveOnSlack({...toSessionSendOptions(r),target:r.target,message:t},{from:e.from,api:e.api,codeChannels:e.codeChannels,credentials:e.credentials,installationTeamId:e.installationTeamId,teamId:n,...typeof e.envelope.event.user==`string`?{triggeringUserId:e.envelope.event.user}:{}}),slack:buildSlackWorkspaceHandle({api:e.api,botToken:e.credentials?.botToken,installationTeamId:e.installationTeamId,teamId:n}),waitUntil(e){r.push(e)}};try{await e.handler(i,e.envelope.event)}catch(t){logError(log,`event handler failed`,t,{eventId:e.envelope.event_id,eventType:e.envelope.event.type})}await Promise.allSettled(r)}async function verifyInbound(e,t){try{return await verifySlackRequest$1(e,{signingSecret:t?.signingSecret??(t?.webhookVerifier?void 0:process.env.SLACK_SIGNING_SECRET),webhookVerifier:t?.webhookVerifier})}catch(e){return log.warn(`slack inbound verification failed`,{error:e}),null}}async function deliverSlackMessage(e){let{message:t,thread:n}=e;try{let r=e.threadContext===void 0?[]:await loadThreadContextMessages(n,t,e.threadContext),i=r.length>0&&e.result.history===void 0&&await e.sessionOperations.resolveSession()===void 0,a=i?void 0:formatSlackThreadContext(r),o=e.result.history??(i?slackThreadSessionHistory(r):void 0),s=await collectInboundFileParts({botUserId:e.botUserId,fetchThreadWindow:(e,t)=>fetchSlackThreadWindow(n,e,t),isMentioned:e.kind===`app_mention`,mention:t,thread:n,policy:e.uploadPolicy}),c={botUserId:e.botUserId,channelId:t.channelId,fullName:t.author?.fullName,isMentioned:e.isMentioned,teamId:t.teamId,threadTs:t.threadTs,userId:t.author?.userId??``,userName:t.author?.userName},l=formatSlackInboundMessage(c,t),u=buildSlackTurnMessage(a===void 0?l:`${a}\n\n${l}`,s),d=e.result.context??[],f=e.isPrivateConversation?`Private message`:e.result.title??t.markdown,p=e.resolvesTypedInput?slackTypedText(t.text,e.botUserId):``,m=attachInputText({auth:e.result.auth,title:f,...d.length>0&&{context:d},...o!==void 0&&{history:o}},s.length===0?p:void 0);await e.sessionOperations.send(u,m)}catch(n){logError(log,`${e.kind} delivery failed`,n,{channelId:t.channelId})}}function slackTypedText(e,t){return t===void 0?e:e.replace(RegExp(`<@${t}(?:\\|[^>]*)?>`,`gu`),``).trim()}async function cancelledARunningTurn(e,t){try{let{outcome:n}=await(await e(slackCodeChannelContinuationToken(t)).cancel()).result({signal:AbortSignal.timeout(1e4)});return n!==`ignored`}catch(e){if(e instanceof SessionNotActiveError)return!1;throw e}}function followReplyThread(e,t){t.message!==void 0&&e.codeChannel===!0&&(e.pendingReplyThreadTs=t.state?.replyThreadTs??null)}function followTriggeringMessage(e,t){if(t.message===void 0)return;let n=t.state?.triggeringMessageTs;typeof n==`string`&&(e.triggeringMessageTs=n)}function recordSlackPrincipal(e,t,n){if(t===null)return;let r=n.state?.triggeringUserId,i=slackUserIdFromAuthContext(t)??(typeof r==`string`?r:void 0);i!==void 0&&(e.slackUsersByPrincipal={...e.slackUsersByPrincipal,[t.principalId]:i})}export{slackChannel};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TaskCard } from "#channel/task-card.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SlackCardMessage } from "#public/channels/slack/renderers.js";
|
|
3
3
|
import type { SlackChannelInternalEvents } from "#public/channels/slack/slackChannel.js";
|
|
4
4
|
/**
|
|
5
5
|
* eve's default task card: a `task_card` block for one task, or a `plan` block
|
|
@@ -8,7 +8,7 @@ import type { SlackChannelInternalEvents } from "#public/channels/slack/slackCha
|
|
|
8
8
|
* with `eve__task_cancel` shows as done, since it no longer needed the work; a
|
|
9
9
|
* failed task, or one stopped any other way, shows as an error.
|
|
10
10
|
*/
|
|
11
|
-
export declare function renderDefaultSlackTaskCard(snapshot: TaskCard):
|
|
11
|
+
export declare function renderDefaultSlackTaskCard(snapshot: TaskCard): SlackCardMessage | null;
|
|
12
12
|
/** The card message eve last wrote for a turn: the card that begins at `since`. */
|
|
13
13
|
export interface SlackTaskCardState {
|
|
14
14
|
readonly ts?: string;
|
|
@@ -21,4 +21,4 @@ export interface SlackTaskCardState {
|
|
|
21
21
|
* the renderer chain's own `committed`, so the card never depends on which handlers a renderer
|
|
22
22
|
* keeps, and writes each card `taskCard` returns if it changed.
|
|
23
23
|
*/
|
|
24
|
-
export declare function withTaskCards(events: SlackChannelInternalEvents, taskCard: (card: TaskCard) =>
|
|
24
|
+
export declare function withTaskCards(events: SlackChannelInternalEvents, taskCard: (card: TaskCard) => SlackCardMessage | null): SlackChannelInternalEvents;
|