eve 0.84.0 → 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 +46 -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 +13 -11
- package/dist/src/channel/cross-channel-receive.js +1 -1
- package/dist/src/channel/delivery.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/channel/types.d.ts +2 -0
- package/dist/src/chunks/{use-eve-agent-BbbguIdB.js → use-eve-agent-B2jCfcnG.js} +29 -12
- package/dist/src/chunks/{use-eve-agent-DWjyzplb.js → use-eve-agent-DCLFi26P.js} +29 -12
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/client/client.js +1 -1
- package/dist/src/client/delivery.d.ts +2 -0
- package/dist/src/client/delivery.js +1 -1
- package/dist/src/client/open-stream.js +1 -1
- package/dist/src/client/optimistic-message-submissions.js +1 -1
- package/dist/src/client/session-store.js +1 -1
- package/dist/src/client/session.js +1 -1
- package/dist/src/client/sessions.d.ts +1 -0
- package/dist/src/client/sessions.js +1 -1
- package/dist/src/client/types.d.ts +7 -4
- package/dist/src/compiled/.vendor-stamp.json +2 -2
- package/dist/src/compiled/@chat-adapter/slack/index.js +13 -13
- package/dist/src/compiled/@vercel/sandbox/api-client/api-client.d.ts +75 -0
- package/dist/src/compiled/@vercel/sandbox/api-client/validators.d.ts +4 -0
- package/dist/src/compiled/@vercel/sandbox/command.d.ts +78 -0
- package/dist/src/compiled/@vercel/sandbox/drive.d.ts +41 -1
- package/dist/src/compiled/@vercel/sandbox/index.js +13 -13
- package/dist/src/compiled/@vercel/sandbox/network-policy.d.ts +35 -4
- package/dist/src/compiled/@vercel/sandbox/sandbox.d.ts +18 -4
- package/dist/src/compiled/@vercel/sandbox/session.d.ts +29 -3
- package/dist/src/compiled/@vercel/sandbox/utils/pipe-stdin.d.ts +19 -0
- package/dist/src/compiled/_chunks/client/{core-CspsxXR-.js → core-BsvaaMUJ.js} +1 -1
- package/dist/src/compiled/_chunks/node/{auth-Dztlx9pE.js → auth-v5pS3mLo.js} +1 -1
- package/dist/src/compiled/_chunks/node/is-stream-DsuQX_bd.js +1 -0
- package/dist/src/compiled/_chunks/node/token-DHGH-jVc.js +1 -0
- package/dist/src/compiled/_chunks/node/token-util-BH2pw9Qm.js +25 -0
- package/dist/src/compiled/_chunks/node/token-util-CcvxNv76.js +1 -0
- package/dist/src/compiled/_chunks/node/version-C2PfNyEv.js +1 -0
- package/dist/src/compiled/zod/index.js +1 -1
- package/dist/src/compiled/zod/v4/core.js +1 -1
- package/dist/src/compiled/zod-validation-error/index.js +1 -1
- package/dist/src/compiler/artifacts.d.ts +1 -0
- package/dist/src/compiler/artifacts.js +1 -1
- package/dist/src/compiler/manifest.d.ts +2 -2
- package/dist/src/compiler/manifest.js +1 -1
- package/dist/src/compiler/model-catalog.d.ts +8 -18
- package/dist/src/compiler/model-catalog.js +1 -1
- package/dist/src/compiler/normalize-agent-config.js +1 -1
- package/dist/src/compiler/normalize-manifest-types.d.ts +3 -0
- package/dist/src/compiler/normalize-manifest.js +1 -1
- package/dist/src/context/keys.d.ts +7 -8
- package/dist/src/context/keys.js +1 -1
- package/dist/src/eve-channel/index.js +1 -1
- package/dist/src/eve-channel/request.d.ts +1 -0
- package/dist/src/eve-channel/request.js +1 -1
- package/dist/src/eve-channel/support.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/agent-sessions/steps.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/sandbox/bindings/docker-cli.d.ts +2 -0
- package/dist/src/execution/sandbox/bindings/docker-cli.js +1 -1
- package/dist/src/execution/sandbox/bindings/docker-connection.js +1 -1
- package/dist/src/execution/sandbox/bindings/just-bash-runtime.js +1 -1
- package/dist/src/execution/sandbox/bindings/vercel.js +1 -1
- package/dist/src/execution/sandbox/multiplexed-command.d.ts +11 -1
- package/dist/src/execution/sandbox/multiplexed-command.js +1 -1
- package/dist/src/execution/sandbox/session.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/session-inbox/address.d.ts +1 -0
- package/dist/src/execution/session-inbox/handle.d.ts +7 -0
- package/dist/src/execution/session-inbox/handle.js +1 -0
- package/dist/src/execution/session-inbox/owner.d.ts +4 -4
- package/dist/src/execution/session-inbox/owner.js +1 -1
- package/dist/src/execution/session-inbox/resume.d.ts +1 -0
- package/dist/src/execution/session-inbox/resume.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.d.ts +3 -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/semantic-errors/rules/gateway.js +1 -1
- package/dist/src/harness/session-machine/transitions.js +1 -1
- package/dist/src/internal/application/compiled-artifacts.d.ts +2 -0
- package/dist/src/internal/application/compiled-artifacts.js +2 -2
- package/dist/src/internal/application/optional-package-install.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/internal/model-catalog.d.ts +39 -16
- package/dist/src/internal/model-catalog.js +1 -1
- package/dist/src/internal/nitro/host/dev-build-host.js +1 -1
- package/dist/src/internal/nitro/routes/channel-dispatch.js +1 -1
- package/dist/src/protocol/delivery.d.ts +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/nuxt/module.js +1 -1
- package/dist/src/public/schedules/subscription.d.ts +10 -1
- package/dist/src/reactions/kinds/model.js +1 -1
- package/dist/src/reactions/kinds/subagent.js +1 -1
- package/dist/src/runtime/agent/resolve-model.d.ts +3 -4
- package/dist/src/runtime/agent/resolve-model.js +1 -1
- package/dist/src/runtime/loaders/artifact-paths.d.ts +1 -0
- package/dist/src/runtime/loaders/artifact-paths.js +1 -1
- package/dist/src/runtime/loaders/bundled-artifacts.d.ts +2 -0
- package/dist/src/runtime/loaders/bundled-artifacts.js +1 -1
- package/dist/src/runtime/loaders/model-metadata.d.ts +4 -0
- package/dist/src/runtime/loaders/model-metadata.js +1 -0
- package/dist/src/runtime/resolve-agent.js +1 -1
- package/dist/src/runtime/sessions/compiled-agent-cache.d.ts +3 -0
- package/dist/src/runtime/sessions/compiled-agent-cache.js +1 -1
- package/dist/src/runtime/subagents/dynamic-agent-config.d.ts +2 -2
- package/dist/src/runtime/subagents/dynamic-agent-config.js +1 -1
- package/dist/src/runtime/subagents/workflow.js +1 -1
- package/dist/src/sandbox/provider.d.ts +2 -0
- package/dist/src/setup/flows/model-source-change.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/agent-definition.d.ts +0 -1
- package/dist/src/shared/channel-definition.d.ts +4 -4
- package/dist/src/shared/sandbox-session.d.ts +4 -0
- 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/agent-config.md +5 -4
- 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/default-harness.md +6 -0
- package/docs/concepts/upgrade-to-v27-events.md +2 -2
- package/docs/evals/judge.mdx +13 -0
- package/docs/experimental/dynamic-schedules.mdx +2 -2
- package/docs/guides/client/overview.mdx +24 -2
- 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/reference/cli.md +1 -1
- package/docs/sandbox/custom-providers.mdx +1 -1
- package/docs/sandbox/index.mdx +1 -1
- package/docs/schedules.mdx +1 -1
- package/docs/tools/overview.mdx +34 -0
- package/docs/tools/workflows.mdx +51 -5
- package/package.json +2 -2
- package/dist/src/compiled/_chunks/node/token-Bedbhv5z.js +0 -1
- package/dist/src/compiled/_chunks/node/token-util-DpJNusVS.js +0 -1
- package/dist/src/compiled/_chunks/node/token-util-DwHQ2a3c.js +0 -1
- package/dist/src/compiled/_chunks/node/version-C__MqbLM.js +0 -1
- package/dist/src/public/channels/slack/approval-cards.d.ts +0 -25
- package/dist/src/public/channels/slack/approval-cards.js +0 -2
- package/dist/src/runtime/agent/model-catalog.d.ts +0 -10
- package/dist/src/runtime/agent/model-catalog.js +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{parseAnswer}from"#shared/input.js";import{truncate}from"#public/channels/text.js";import{SLACK_SECTION_TEXT_MAX_LENGTH,truncateCardBodyText,truncateModalTitle,truncatePlainText,truncateSectionText}from"#public/channels/slack/limits.js";const ANSWER_ACTION_PREFIX=`eve_input:`,HITL_FREEFORM_ACTION_PREFIX=`eve_input_freeform:`,HITL_FREEFORM_MODAL_CALLBACK_ID=`eve_input_freeform_submit`,HITL_FREEFORM_MODAL_BLOCK_ID=`eve_freeform_block`,HITL_FREEFORM_MODAL_ACTION_ID=`eve_freeform_text`,HITL_ROUTE_PREFIX=`${ANSWER_ACTION_PREFIX}route:`,BUTTON_ACTION_ID_RE=/^(?:(?<kind>tool-approval):)?(?<requestId>.+):button:\d+$/u,TOOL_INPUT_SUFFIX="\n```",answerFromSlackAction=e=>decodeHitlResponse(e);function decodeHitlResponse(e){let t=decodeHitlActionId(e.actionId);if(t===null)return null;let n=e.selectedOptionValue??e.value;if(n===void 0||e.value!==void 0&&!t.button)return null;let r={response:parseAnswer({interactionId:t.requestId,optionId:n})};return t.kind!==void 0&&(r.kind=t.kind),t.route!==void 0&&(r.route=t.route),r}function splitEncodedRequest(e){return e.startsWith(`tool-approval:`)?{kind:`tool-approval`,requestId:e.slice(14)}:{requestId:e}}function isHitlAction(e){return e.startsWith(ANSWER_ACTION_PREFIX)}function decodeHitlActionId(e){if(!e.startsWith(`eve_input:`))return null;let t=e.slice(10),n;if(t.startsWith(`route:`)){t=t.slice(6);let e=t.indexOf(`:`),r=t.indexOf(`:`,e+1);if(e<=0||r<=e+1)return null;n={channelId:t.slice(0,e),threadTs:t.slice(e+1,r)},t=t.slice(r+1)}let r=BUTTON_ACTION_ID_RE.exec(t),i=splitEncodedRequest(r?.groups?.requestId??t);if(!i.requestId)return null;let a=r?.groups?.kind===`tool-approval`?`tool-approval`:i.kind,o={button:r!==null,requestId:i.requestId};return a!==void 0&&(o.kind=a),n!==void 0&&(o.route=n),o}function encodeHitlActionId(e,t){let n=`${e.request.kind===`approval`?`tool-approval:`:``}${e.interactionId}`;return t===void 0?`${ANSWER_ACTION_PREFIX}${n}`:`${HITL_ROUTE_PREFIX}${t.channelId}:${t.threadTs}:${n}`}function renderInteractionBlocks(e,t){let n={text:{text:truncateSectionText(e.request.prompt),type:`mrkdwn`},type:`section`},r=renderInteractionDetailBlocks(e),i=encodeHitlActionId(e,t),a=e.request.options,o=e.request.allowFreeform===!0||!a||a.length===0;if(a&&a.length>0&&e.request.display===`select`){let e=a.length<=6?{type:`radio_buttons`,action_id:i,options:a.map(buildOption)}:{type:`static_select`,action_id:i,options:a.map(buildOption),placeholder:{type:`plain_text`,text:`Choose an option`}};return[n,...r,{type:`actions`,elements:[e]}]}if(a&&a.length>0){let t=renderInteractionCardBlock(e,i),n=renderToolInputContainerBlock(e);return n===void 0?[t]:[t,n]}if(o){let t=i.replace(ANSWER_ACTION_PREFIX,HITL_FREEFORM_ACTION_PREFIX);return[n,...r,{type:`actions`,elements:[{type:`button`,action_id:t,text:{type:`plain_text`,text:`Type your answer`},style:`primary`,value:e.interactionId}]}]}return[n]}function renderInteractionPostParts(e,t){let n=renderInteractionBlocks(e,t),r=n
|
|
1
|
+
import{parseAnswer}from"#shared/input.js";import{truncate}from"#public/channels/text.js";import{SLACK_SECTION_TEXT_MAX_LENGTH,truncateCardBodyText,truncateModalTitle,truncatePlainText,truncateSectionText}from"#public/channels/slack/limits.js";const ANSWER_ACTION_PREFIX=`eve_input:`,HITL_FREEFORM_ACTION_PREFIX=`eve_input_freeform:`,HITL_FREEFORM_MODAL_CALLBACK_ID=`eve_input_freeform_submit`,HITL_FREEFORM_MODAL_BLOCK_ID=`eve_freeform_block`,HITL_FREEFORM_MODAL_ACTION_ID=`eve_freeform_text`,HITL_ROUTE_PREFIX=`${ANSWER_ACTION_PREFIX}route:`,HITL_CODE_CHANNEL_ROUTE_PREFIX=`${ANSWER_ACTION_PREFIX}code:`,BUTTON_ACTION_ID_RE=/^(?:(?<kind>tool-approval):)?(?<requestId>.+):button:\d+$/u,TOOL_INPUT_SUFFIX="\n```",answerFromSlackAction=e=>decodeHitlResponse(e);function decodeHitlResponse(e){let t=decodeHitlActionId(e.actionId);if(t===null)return null;let n=e.selectedOptionValue??e.value;if(n===void 0||e.value!==void 0&&!t.button)return null;let r={response:parseAnswer({interactionId:t.requestId,optionId:n})};return t.kind!==void 0&&(r.kind=t.kind),t.route!==void 0&&(r.route=t.route),r}function splitEncodedRequest(e){return e.startsWith(`tool-approval:`)?{kind:`tool-approval`,requestId:e.slice(14)}:{requestId:e}}function isHitlAction(e){return e.startsWith(ANSWER_ACTION_PREFIX)}function decodeHitlActionId(e){if(!e.startsWith(`eve_input:`))return null;let t=e.slice(10),n;if(t.startsWith(`route:`)){t=t.slice(6);let e=t.indexOf(`:`),r=t.indexOf(`:`,e+1);if(e<=0||r<=e+1)return null;n={channelId:t.slice(0,e),threadTs:t.slice(e+1,r)},t=t.slice(r+1)}else if(t.startsWith(`code:`)){t=t.slice(5);let e=t.indexOf(`:`);if(e<=0)return null;n={channelId:t.slice(0,e),codeChannel:!0},t=t.slice(e+1)}let r=BUTTON_ACTION_ID_RE.exec(t),i=splitEncodedRequest(r?.groups?.requestId??t);if(!i.requestId)return null;let a=r?.groups?.kind===`tool-approval`?`tool-approval`:i.kind,o={button:r!==null,requestId:i.requestId};return a!==void 0&&(o.kind=a),n!==void 0&&(o.route=n),o}function encodeHitlActionId(e,t){let n=`${e.request.kind===`approval`?`tool-approval:`:``}${e.interactionId}`;return t===void 0?`${ANSWER_ACTION_PREFIX}${n}`:t.codeChannel===!0?`${HITL_CODE_CHANNEL_ROUTE_PREFIX}${t.channelId}:${n}`:`${HITL_ROUTE_PREFIX}${t.channelId}:${t.threadTs}:${n}`}function renderInteractionBlocks(e,t){let n={text:{text:truncateSectionText(e.request.prompt),type:`mrkdwn`},type:`section`},r=renderInteractionDetailBlocks(e),i=encodeHitlActionId(e,t),a=e.request.options,o=e.request.allowFreeform===!0||!a||a.length===0;if(a&&a.length>0&&e.request.display===`select`){let e=a.length<=6?{type:`radio_buttons`,action_id:i,options:a.map(buildOption)}:{type:`static_select`,action_id:i,options:a.map(buildOption),placeholder:{type:`plain_text`,text:`Choose an option`}};return[n,...r,{type:`actions`,elements:[e]}]}if(a&&a.length>0){let t=renderInteractionCardBlock(e,i),n=renderToolInputContainerBlock(e);return n===void 0?[t]:[t,n]}if(o){let t=i.replace(ANSWER_ACTION_PREFIX,HITL_FREEFORM_ACTION_PREFIX);return[n,...r,{type:`actions`,elements:[{type:`button`,action_id:t,text:{type:`plain_text`,text:`Type your answer`},style:`primary`,value:e.interactionId}]}]}return[n]}function renderInteractionPostParts(e,t){let n=renderInteractionBlocks(e,t),[r,...i]=n;return!isApprovalRequest(e)||r===void 0||!isBlockType(r,`card`)||i.length===0?{controls:{blocks:n,text:formatInteractionFallbackText(e)}}:{controls:{blocks:[r],text:e.request.prompt},details:{blocks:i,text:formatInteractionFallbackText(e)}}}function formatInteractionFallbackText(e){let t=formatToolInputDetails(e);return t===void 0?e.request.prompt:`${e.request.prompt}\n${t}`}function buildFreeformModalView(e){let t=e.prompt?truncateModalTitle(e.prompt):`Your answer`,n=e.prompt?[{type:`section`,text:{type:`mrkdwn`,text:truncateSectionText(e.prompt)}}]:[];return{type:`modal`,callback_id:HITL_FREEFORM_MODAL_CALLBACK_ID,private_metadata:JSON.stringify(e.metadata),title:{type:`plain_text`,text:t},submit:{type:`plain_text`,text:`Submit`},close:{type:`plain_text`,text:`Cancel`},blocks:[...n,{type:`input`,block_id:HITL_FREEFORM_MODAL_BLOCK_ID,element:{type:`plain_text_input`,action_id:HITL_FREEFORM_MODAL_ACTION_ID,multiline:!0,placeholder:{type:`plain_text`,text:`Type your answer here...`}},label:{type:`plain_text`,text:`Answer`}}]}}function isFreeformAction(e){return e.startsWith(HITL_FREEFORM_ACTION_PREFIX)}function decodeFreeformHitlActionId(e){return isFreeformAction(e)?decodeHitlActionId(`${ANSWER_ACTION_PREFIX}${e.slice(19)}`):null}function buildCardButton(e,t,n){let r={type:`button`,text:{type:`plain_text`,text:truncatePlainText(e.label),emoji:!1},action_id:`${t}:button:${n}`,value:e.id};return(e.style===`primary`||e.style===`danger`)&&(r.style=e.style),r}function buildOption(e){let t={text:{text:truncatePlainText(e.label),type:`plain_text`},value:e.id},n=truncatePlainText(e.description);return n&&n.length>0&&(t.description={text:n,type:`plain_text`}),t}function renderInteractionCardBlock(e,t){let n={type:`card`,body:{type:`mrkdwn`,text:truncateCardBodyText(`*${e.request.prompt}*`),verbatim:!1},actions:cardButtonOptions(e).map((e,n)=>buildCardButton(e,t,n))};return isApprovalRequest(e)&&(n.subtext={type:`mrkdwn`,text:`I've paused until someone approves or denies.`}),n}function cardButtonOptions(e){let t=e.request.options??[];if(!isApprovalRequest(e))return t.map(toCardButtonOption);let n=t.find(e=>e.id===`approve`),r=t.find(e=>e.id===`deny`);return!n||!r?t.map(toCardButtonOption):[{id:r.id,label:`Deny`},{id:n.id,label:`Approve`,style:`primary`}]}function toCardButtonOption(e){let t={id:e.id,label:e.label};return e.style===`primary`||e.style===`danger`?{...t,style:e.style}:t}function buildAnsweredBlocks(e){let t=[];for(let n of e.promptBlocks)n!=null&&t.push(n);let n=truncate(e.answerLabel,SLACK_SECTION_TEXT_MAX_LENGTH-20-1);return t.push({type:`section`,text:{type:`mrkdwn`,text:`:white_check_mark: *${n}*`}}),e.userId&&e.userId.length>0&&t.push({type:`context`,elements:[{type:`mrkdwn`,text:`Answered by <@${e.userId}>`}]}),t}function renderInteractionDetailBlocks(e){let t=formatToolInputDetails(e);return t===void 0?[]:[{type:`section`,text:{type:`mrkdwn`,text:t}}]}function renderToolInputContainerBlock(e){let t=formatToolInputContainerText(e);if(t!==void 0)return{type:`container`,title:{type:`plain_text`,text:`Tool input`},is_collapsible:!0,default_collapsed:!0,child_blocks:[{type:`section`,text:{type:`mrkdwn`,text:t}}]}}function formatToolInputContainerText(e){if(!isApprovalRequest(e))return;let t=JSON.stringify(e.call?.input??{},null,2);if(t===`{}`)return;let n=SLACK_SECTION_TEXT_MAX_LENGTH-4-4;return`\`\`\`
|
|
2
2
|
${truncate(t,n)}${TOOL_INPUT_SUFFIX}`}function formatToolInputDetails(e){if(!isApprovalRequest(e))return;let t=JSON.stringify(e.call?.input??{},null,2);if(t===`{}`)return;let n=SLACK_SECTION_TEXT_MAX_LENGTH-17-4;return`*Tool input*
|
|
3
3
|
\`\`\`
|
|
4
4
|
${truncate(t,n)}${TOOL_INPUT_SUFFIX}`}function isBlockType(e,t){return typeof e==`object`&&!!e&&e.type===t}function isApprovalRequest(e){return e.request.kind===`approval`}export{ANSWER_ACTION_PREFIX,HITL_FREEFORM_ACTION_PREFIX,HITL_FREEFORM_MODAL_ACTION_ID,HITL_FREEFORM_MODAL_BLOCK_ID,HITL_FREEFORM_MODAL_CALLBACK_ID,answerFromSlackAction,buildAnsweredBlocks,buildFreeformModalView,decodeFreeformHitlActionId,decodeHitlActionId,decodeHitlResponse,formatInteractionFallbackText,isFreeformAction,isHitlAction,renderInteractionBlocks,renderInteractionPostParts};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { openCodeChannel, type SlackCodeChannelOpenResult, } from "#public/channels/slack/open-code-channel.js";
|
|
2
|
+
export { slackChannel, type SlackApiResponse, type SlackApprovalChannel, type SlackAuthorizationEventContext, type SlackBotToken, type SlackChannel, type SlackChannelConfig, type SlackChannelContext, type SlackChannelCredentials, type SlackChannelState, type SlackCodeChannelTarget, type SlackContext, type SlackEventContext, type SlackEventRespondFn, type SlackEventRespondOptions, type SlackEventSendFn, type SlackEventSendOptions, type SlackHandle, type SlackInboundEventContext, type SlackInboundMessageContext, type SlackOpenCodeChannelOptions, type SlackOpenedCodeChannel, type SlackInboundResult, type SlackInitialMessage, type SlackAnswerContext, type SlackAnswerResult, type SlackAnswerSubmission, type SlackInstrumentationMetadata, type SlackInteractionAction, type SlackInteractionContext, type SlackSlashCommand, type SlackSlashCommandContext, type SlackShortcut, type SlackShortcutContext, type SlackShortcutMessage, type SlackMentionResult, type SlackReceiveTarget, type SlackSessionTarget, type SlackRespondOptions, type SlackSendOptions, type SlackSessionOperations, type SlackThread, type SlackWebhookVerifier, type SlackWorkspaceHandle, } from "#public/channels/slack/slackChannel.js";
|
|
2
3
|
export type { SlackAttachment, SlackAuthor, SlackEvent, SlackEventEnvelope, SlackInboundContext, SlackMessage, } from "#public/channels/slack/inbound.js";
|
|
3
|
-
export { callSlackApi, resolveSlackBotToken, slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackBotTokenContext, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
|
|
4
|
+
export { callSlackApi, resolveSlackBotToken, slackCodeChannelContinuationToken, slackContinuationToken, type SlackPostInput, type SlackPostedMessage, type SlackBotTokenContext, type SlackThreadMessage, type SlackUploadFilesOptions, type SlackUploadFilesResult, } from "#public/channels/slack/api.js";
|
|
4
5
|
export { type SlackFetch, type SlackTransportOptions } from "#public/channels/slack/transport.js";
|
|
5
6
|
export { defaultSlackAuth } from "#public/channels/slack/defaults.js";
|
|
6
7
|
export { postCompletedSlackReply, type SlackCompletedReply, } from "#public/channels/slack/reply-delivery.js";
|
|
7
|
-
export { defineSlackRenderer, type SlackRenderer, type SlackRendererEvents, type SlackRenderHandler, type SlackRenderNext,
|
|
8
|
+
export { defineSlackRenderer, type SlackCardMessage, type SlackInteractionCard, type SlackInteractionCardParts, type SlackRenderer, type SlackRendererEvents, type SlackRenderHandler, type SlackRenderNext, } from "#public/channels/slack/renderers.js";
|
|
8
9
|
export type { TaskCard } from "#channel/task-card.js";
|
|
9
10
|
export { loadThreadContextMessages, type LoadThreadContextMessagesOptions, type ThreadContextSince, } from "#public/channels/slack/thread.js";
|
|
10
11
|
export { cardToBlocks, cardToFallbackText, type BlockKitBlock, } from "#public/channels/slack/blocks.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{callSlackApi,resolveSlackBotToken,slackContinuationToken}from"#public/channels/slack/api.js";import{Actions,Button,Card,CardLink,CardText,Divider,ExternalSelect,Field,Fields,Image,LinkButton,Modal,RadioSelect,Section,Select,SelectOption,Table,TextInput,cardChildToFallbackText,isCardElement}from"#public/channels/slack/cards.js";import{cardToBlocks,cardToFallbackText}from"#public/channels/slack/blocks.js";import{
|
|
1
|
+
import{callSlackApi,resolveSlackBotToken,slackCodeChannelContinuationToken,slackContinuationToken}from"#public/channels/slack/api.js";import{Actions,Button,Card,CardLink,CardText,Divider,ExternalSelect,Field,Fields,Image,LinkButton,Modal,RadioSelect,Section,Select,SelectOption,Table,TextInput,cardChildToFallbackText,isCardElement}from"#public/channels/slack/cards.js";import{cardToBlocks,cardToFallbackText}from"#public/channels/slack/blocks.js";import{postCompletedSlackReply}from"#public/channels/slack/reply-delivery.js";import{openCodeChannel}from"#public/channels/slack/open-code-channel.js";import{slackChannel}from"#public/channels/slack/slackChannel.js";import{defaultSlackAuth}from"#public/channels/slack/defaults.js";import{defineSlackRenderer}from"#public/channels/slack/renderers.js";import{loadThreadContextMessages}from"#public/channels/slack/thread.js";import{ANSWER_ACTION_PREFIX,answerFromSlackAction,renderInteractionBlocks}from"#public/channels/slack/hitl.js";export{ANSWER_ACTION_PREFIX,Actions,Button,Card,CardLink,CardText,Divider,ExternalSelect,Field,Fields,Image,LinkButton,Modal,RadioSelect,Section,Select,SelectOption,Table,TextInput,answerFromSlackAction,callSlackApi,cardChildToFallbackText,cardToBlocks,cardToFallbackText,defaultSlackAuth,defineSlackRenderer,isCardElement,loadThreadContextMessages,openCodeChannel,postCompletedSlackReply,renderInteractionBlocks,resolveSlackBotToken,slackChannel,slackCodeChannelContinuationToken,slackContinuationToken};
|
|
@@ -15,8 +15,6 @@ export declare function answeredBlocksFromActionBlock(input: {
|
|
|
15
15
|
readonly block: unknown;
|
|
16
16
|
readonly userId?: string;
|
|
17
17
|
}): unknown[] | undefined;
|
|
18
|
-
/** Settles a pressed HITL card as answered by the presser with the choice they pressed. */
|
|
19
|
-
export declare function updateAnsweredHitlCard(interaction: ParsedBlockActionsPayload, deps: InteractionHandlerDeps): Promise<void>;
|
|
20
18
|
/** Replaces a pressed HITL card's controls with `card`. */
|
|
21
19
|
export declare function settleHitlCard(interaction: ParsedBlockActionsPayload, deps: InteractionHandlerDeps, card: SettledCard): Promise<void>;
|
|
22
20
|
/** Replaces a freeform card's controls with `card`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{truncate}from"#public/channels/text.js";import{callSlackApi}from"#public/channels/slack/api.js";import{SLACK_CARD_SUBTEXT_MAX_LENGTH,truncateCardSubtext}from"#public/channels/slack/limits.js";import{buildAnsweredBlocks,isHitlAction}from"#public/channels/slack/hitl.js";function buildSettledHitlMessageBlocks(e){let t=e.messageBlocks.findIndex(t=>blockContainsActionId(t,e.actionId));if(t===-1)return buildAnsweredBlocks({promptBlocks:findPromptBlocks(e.messageBlocks),answerLabel:e.card.label,userId:e.card.userId});let n=e.messageBlocks[t],r=answeredBlocksFromActionBlock({answerLabel:e.card.label,block:n,userId:e.card.userId})??buildAnsweredBlocks({promptBlocks:promptBlocksFromActionBlock(n),answerLabel:e.card.label,userId:e.card.userId});return[...e.messageBlocks.slice(0,t),...r,...e.messageBlocks.slice(t+1)]}function findPromptBlocks(e){let t=[];for(let n of e){if(!isObjectRecord(n))continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function blockContainsActionId(e,t){return isObjectRecord(e)?actionsContainActionId(e.elements,t)||actionsContainActionId(e.actions,t):!1}function actionsContainActionId(e,t){return Array.isArray(e)?e.some(e=>isObjectRecord(e)&&e.action_id===t):!1}function answeredBlocksFromActionBlock(e){if(!isObjectRecord(e.block)||e.block.type!==`card`)return;let{actions:t,subtext:n,...r}=e.block,i={...r,subtext:{type:`mrkdwn`,text:formatAnsweredCardSubtext(e),verbatim:!1}};return hasCardContent(i)?[i]:void 0}function formatAnsweredCardSubtext(e){let t=e.userId===void 0?``:` by <@${e.userId}>`,n=SLACK_CARD_SUBTEXT_MAX_LENGTH-20-1-t.length,r=truncate(e.answerLabel,n);return truncateCardSubtext(`:white_check_mark: *${r}*${t}`)}function promptBlocksFromActionBlock(e){if(!isObjectRecord(e)||e.type!==`card`)return[];let{actions:t,...n}=e;return hasCardContent(n)?[n]:[]}function hasCardContent(e){return e.body!==void 0||e.title!==void 0||e.hero_image!==void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}async function
|
|
1
|
+
import{truncate}from"#public/channels/text.js";import{callSlackApi}from"#public/channels/slack/api.js";import{SLACK_CARD_SUBTEXT_MAX_LENGTH,truncateCardSubtext}from"#public/channels/slack/limits.js";import{buildAnsweredBlocks,isHitlAction}from"#public/channels/slack/hitl.js";function buildSettledHitlMessageBlocks(e){let t=e.messageBlocks.findIndex(t=>blockContainsActionId(t,e.actionId));if(t===-1)return buildAnsweredBlocks({promptBlocks:findPromptBlocks(e.messageBlocks),answerLabel:e.card.label,userId:e.card.userId});let n=e.messageBlocks[t],r=answeredBlocksFromActionBlock({answerLabel:e.card.label,block:n,userId:e.card.userId})??buildAnsweredBlocks({promptBlocks:promptBlocksFromActionBlock(n),answerLabel:e.card.label,userId:e.card.userId});return[...e.messageBlocks.slice(0,t),...r,...e.messageBlocks.slice(t+1)]}function findPromptBlocks(e){let t=[];for(let n of e){if(!isObjectRecord(n))continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function blockContainsActionId(e,t){return isObjectRecord(e)?actionsContainActionId(e.elements,t)||actionsContainActionId(e.actions,t):!1}function actionsContainActionId(e,t){return Array.isArray(e)?e.some(e=>isObjectRecord(e)&&e.action_id===t):!1}function answeredBlocksFromActionBlock(e){if(!isObjectRecord(e.block)||e.block.type!==`card`)return;let{actions:t,subtext:n,...r}=e.block,i={...r,subtext:{type:`mrkdwn`,text:formatAnsweredCardSubtext(e),verbatim:!1}};return hasCardContent(i)?[i]:void 0}function formatAnsweredCardSubtext(e){let t=e.userId===void 0?``:` by <@${e.userId}>`,n=SLACK_CARD_SUBTEXT_MAX_LENGTH-20-1-t.length,r=truncate(e.answerLabel,n);return truncateCardSubtext(`:white_check_mark: *${r}*${t}`)}function promptBlocksFromActionBlock(e){if(!isObjectRecord(e)||e.type!==`card`)return[];let{actions:t,...n}=e;return hasCardContent(n)?[n]:[]}function hasCardContent(e){return e.body!==void 0||e.title!==void 0||e.hero_image!==void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}async function settleHitlCard(e,t,n){let r=e.actions.find(e=>isHitlAction(e.actionId));r?.messageTs&&await updateSettledCard({blocks:buildSettledHitlMessageBlocks({actionId:r.actionId,card:n,messageBlocks:e.messageBlocks}),card:n,channelId:e.channelId,deps:t,installationTeamId:e.installationTeamId,messageTs:r.messageTs})}async function settleFreeformCard(e){await updateSettledCard({...e,blocks:buildAnsweredBlocks({promptBlocks:[],answerLabel:e.card.label,userId:e.card.userId})})}async function updateSettledCard(e){await callSlackApi({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,context:{teamId:e.installationTeamId},operation:`chat.update`,body:{channel:e.channelId,ts:e.messageTs,blocks:e.blocks,text:e.card.text}})}export{answeredBlocksFromActionBlock,settleFreeformCard,settleHitlCard};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"#internal/logging.js";import{parseAnswer}from"#shared/input.js";import{INACTIVE_REQUEST_NOTICE,respondOrRetireInactivePrompt}from"#channel/inactive-prompt.js";import{buildSlackBinding,buildSlackWorkspaceHandle,callSlackApi,resolveSlackBotToken,slackContinuationToken}from"#public/channels/slack/api.js";import{buildSlackAuthContext}from"#public/channels/slack/auth.js";import{readSlackTextObject}from"#public/channels/slack/inbound-content.js";import{isSlackResponseError}from"#public/channels/slack/transport.js";import{HITL_FREEFORM_MODAL_ACTION_ID,HITL_FREEFORM_MODAL_BLOCK_ID,HITL_FREEFORM_MODAL_CALLBACK_ID,buildFreeformModalView,decodeFreeformHitlActionId,decodeHitlResponse,isFreeformAction,isHitlAction}from"#public/channels/slack/hitl.js";import{settleFreeformCard,settleHitlCard,updateAnsweredHitlCard}from"#public/channels/slack/interaction-cards.js";import{dispatchSignInCancel}from"#public/channels/slack/sign-in-cancel.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{authorizeAnswer}from"#public/channels/slack/answers.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{dispatchSlashCommand}from"#public/channels/slack/slash-command.js";const log=createLogger(`slack.interactions`);function parseBlockActionsPayload(e){if(isSharedBlockActionsPayload(e))return parseSharedBlockActionsPayload(e);let t=e,n=t.actions;if(!Array.isArray(n))return null;let r=t.channel?.id,i=t.message,a=i?.thread_ts??i?.ts;if(!r||!a)return null;let o=t.team,s=t.user,c=s.team_id??o?.id,l={id:s.id,username:s.username,name:s.name},u=i?.blocks??[];return{actions:n.map(e=>({actionId:String(e.action_id??``),value:e.value==null?void 0:String(e.value),blockId:e.block_id==null?void 0:String(e.block_id),selectedOptionValue:extractSelectedOptionValue(e),messageTs:i?.ts,triggerId:typeof t.trigger_id==`string`?t.trigger_id:void 0,label:extractActionLabel(e),user:l})),channelId:r,installationTeamId:readInstallationTeamId(t),threadTs:a,teamId:c,messageBlocks:u}}function isSharedBlockActionsPayload(e){return e.kind===`block_actions`&&Array.isArray(e.actions)}function parseSharedBlockActionsPayload(e){return!e.channelId||!e.threadTs?null:{actions:e.actions.map(t=>({actionId:t.actionId,value:t.value,blockId:t.blockId,selectedOptionValue:t.selectedOptionValue,messageTs:e.messageTs,triggerId:e.triggerId,label:t.label,user:{id:t.user?.id??e.userId,username:t.user?.username,name:t.user?.name}})),channelId:e.channelId,installationTeamId:readInstallationTeamId(e.raw),threadTs:e.threadTs,teamId:e.user?.teamId??e.teamId,messageBlocks:e.messageBlocks??[]}}function extractSelectedOptionValue(e){let t=e.selected_option;return typeof t?.value==`string`?t.value:void 0}function extractActionLabel(e){let t=e.selected_option?.text?.text;if(typeof t==`string`&&t.length>0)return t;let n=e.text?.text;if(typeof n==`string`&&n.length>0)return n}function findPromptBlocks(e){let t=[];for(let n of e){if(typeof n!=`object`||!n)continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function readPromptTextFromBlocks(e){let t=findPromptBlocks(e)[0],n=readSlackTextObject(t?.text);return n.length>0?n:void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}function readInstallationTeamId(e){if(!isObjectRecord(e))return;let t=isObjectRecord(e.view)?e.view:void 0,n=isObjectRecord(e.team)?e.team:void 0,r=isObjectRecord(e.user)?e.user:void 0;return[t?.app_installed_team_id,e.app_installed_team_id,n?.id,r?.team_id,t?.team_id].find(e=>typeof e==`string`&&e.length>0)}async function handleInteractionPost(e,t,n){let r=new Response(`ok`,{status:200}),i;try{i=parseSlackWebhookBody(e,{contentType:`application/x-www-form-urlencoded`})}catch{return log.warn(`failed to parse Slack interaction payload`),r}if(i.kind===`view_submission`)return handleViewSubmission(i,t,n);if(i.kind===`slash_command`)return dispatchSlashCommand(i,t,n),new Response(null,{status:200});if(i.kind===`unsupported`){let e=parseShortcutPayload(i.raw);return e===null?(log.warn(`unsupported Slack interaction payload ignored`,{type:i.type}),r):(dispatchShortcut(e,readInstallationTeamId(i.raw),t,n),new Response(null,{status:200}))}if(i.kind!==`block_actions`)return log.warn(`unsupported Slack interaction payload ignored`,{type:i.kind}),r;if(dispatchSignInCancel(i.raw,t))return r;let a=parseBlockActionsPayload(i);if(!a)return r;let o=a.actions.find(e=>isFreeformAction(e.actionId));if(o)return await openFreeformModal({payload:i.raw,interaction:a,freeformAction:o,deps:n}),r;let s=a.actions.flatMap(e=>{let t=decodeHitlResponse(e);return t===null?[]:[{action:e,derived:t}]});if(s.length>0){let e=s[0].action.user,r=s[0].derived.route;t.waitUntil(dispatchBlockAnswers({ctx:t,deps:n,interaction:a,route:r,submission:{type:`block_actions`,actions:s.map(({action:e})=>e),answers:s.map(({derived:e})=>e.response),messageTs:s[0].action.messageTs,user:e}}))}let c=n.config.onInteraction;if(c){let e=a.actions.filter(e=>!isHitlAction(e.actionId));if(e.length>0){let r=e[0].user,{thread:i,slack:o}=buildSlackBinding({api:n.api,botToken:n.config.credentials?.botToken,channelId:a.channelId,threadTs:a.threadTs,installationTeamId:a.installationTeamId,teamId:a.teamId}),s={...bindSlackSessionOperations({address:slackContinuationToken({channelId:a.channelId,threadTs:a.threadTs}),defaultAuth:buildSlackAuthContext({channelId:a.channelId,installationTeamId:a.installationTeamId,teamId:a.teamId,threadTs:a.threadTs,userId:r.id,userName:r.username??r.name}),from:t.from,resolveSession:t.resolveSession,state:{channelId:a.channelId,installationTeamId:a.installationTeamId??null,teamId:a.teamId??null,threadTs:a.threadTs,triggeringUserId:r.id}}),thread:i,slack:o};for(let n of e)t.waitUntil(Promise.resolve(c(n,s)).catch(e=>{log.error(`custom interaction handler failed`,{error:e})}))}}return r}function parseShortcutPayload(e){if(!isObjectRecord(e))return null;let t=e.type;if(t!==`message_action`&&t!==`shortcut`)return null;let n=readRequiredString(e.callback_id),r=readRequiredString(e.trigger_id),i=isObjectRecord(e.user)?e.user:void 0,a=readRequiredString(i?.id);if(n===null||r===null||a===null)return null;let o=isObjectRecord(e.team)?e.team:void 0,s=readOptionalString(i?.team_id)??readOptionalString(o?.id),c={id:a,username:readOptionalString(i?.username),name:readOptionalString(i?.name)};if(t===`shortcut`)return{type:t,callbackId:n,triggerId:r,user:c,teamId:s};let l=isObjectRecord(e.channel)?e.channel:void 0,u=isObjectRecord(e.message)?e.message:void 0,d=readRequiredString(l?.id),f=readRequiredString(u?.ts);return d===null||f===null||u===void 0?null:{type:t,callbackId:n,triggerId:r,user:c,teamId:s,channelId:d,message:{text:typeof u.text==`string`?u.text:``,ts:f,threadTs:readOptionalString(u.thread_ts),userId:readOptionalString(u.user)},responseUrl:readOptionalString(e.response_url)}}function readRequiredString(e){return typeof e==`string`&&e.length>0?e:null}function readOptionalString(e){return typeof e==`string`&&e.length>0?e:void 0}function dispatchShortcut(e,t,n,r){let i=r.config.onShortcut;if(i===void 0){log.warn(`Slack shortcut ignored because onShortcut is not configured`,{type:e.type});return}let a={slack:buildSlackWorkspaceHandle({api:r.api,botToken:r.config.credentials?.botToken,installationTeamId:t,teamId:e.teamId})};dispatchInteractionHook(()=>i(e,a),n,`shortcut handler failed`)}function dispatchInteractionHook(e,t,n){t.waitUntil(Promise.resolve().then(e).catch(e=>{log.error(n,{error:e})}))}async function dispatchBlockAnswers(e){let t=e.route?.channelId??e.interaction.channelId,n=e.route?.threadTs??e.interaction.threadTs,r=await authorizeAnswer({channelId:t,deps:e.deps,installationTeamId:e.interaction.installationTeamId,submission:e.submission,teamId:e.interaction.teamId,threadTs:n});if(r===null)return;let i=await respondOrRetireInactivePrompt(()=>e.ctx.from(slackContinuationToken({channelId:t,threadTs:n})).respond(e.submission.answers,{auth:r.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleHitlCard(e.interaction,e.deps,{label:t,text:t})},log);if(i.outcome===`inactive`&&await Promise.all([i.retirement,postEphemeralNotice({channelId:e.interaction.channelId,deps:e.deps,installationTeamId:e.interaction.installationTeamId,teamId:e.interaction.teamId,threadTs:e.interaction.threadTs,userId:e.submission.user.id})]),i.outcome===`delivered`&&!e.submission.actions.some(e=>decodeHitlResponse(e)?.kind===`tool-approval`))try{await updateAnsweredHitlCard(e.interaction,e.deps)}catch(e){log.error(`HITL answered-card update failed`,{error:e})}}async function postEphemeralNotice(e){let{thread:t}=buildSlackBinding({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,channelId:e.channelId,installationTeamId:e.installationTeamId,teamId:e.teamId,threadTs:e.threadTs});try{await t.postEphemeral(e.userId,INACTIVE_REQUEST_NOTICE)}catch(e){log.error(`inactive prompt notice failed`,{error:e})}}async function openFreeformModal(e){let t=e.payload.trigger_id;if(typeof t!=`string`||t.length===0){log.warn(`freeform button click missing trigger_id — cannot open modal`);return}let n=decodeFreeformHitlActionId(e.freeformAction.actionId);if(n===null){log.warn(`freeform button click carries invalid metadata`);return}let r=n.requestId;if(!r){log.warn(`freeform button click missing requestId`);return}let i=e.freeformAction.messageTs;if(!i){log.warn(`freeform button click missing messageTs`);return}let a=n?.route?.channelId??e.interaction.channelId,o=n?.route?.threadTs??e.interaction.threadTs,s={continuationToken:slackContinuationToken({channelId:a,threadTs:o}),channelId:a,installationTeamId:e.interaction.installationTeamId,messageChannelId:e.interaction.channelId,threadTs:o,messageTs:i,requestId:r},c=readPromptTextFromBlocks(e.interaction.messageBlocks),l=buildFreeformModalView({metadata:s,prompt:c}),u=await resolveSlackBotToken(e.deps.config.credentials?.botToken,{teamId:e.interaction.installationTeamId});try{await callSlackApi({api:e.deps.api,botToken:u,operation:`views.open`,body:{trigger_id:t,view:l}})}catch(e){if(!isSlackResponseError(e))throw e;log.error(`Slack views.open failed`,{error:e})}}async function handleViewSubmission(e,t,n){let r=new Response(null,{status:200});if(e.callbackId!==HITL_FREEFORM_MODAL_CALLBACK_ID)return r;let i;try{i=JSON.parse(e.privateMetadata??``)}catch{return log.warn(`freeform view_submission carries invalid private_metadata`),r}if(!i.continuationToken||!i.requestId||!i.messageTs||!i.channelId||!i.threadTs)return r;let a=e.values?.find(e=>e.blockId===HITL_FREEFORM_MODAL_BLOCK_ID&&e.actionId===HITL_FREEFORM_MODAL_ACTION_ID)?.value??``;if(a.length===0)return r;let o=e.user,s=e.userId,c=o?.teamId??e.teamId??null,l=i.installationTeamId??readInstallationTeamId(e.raw),u={type:`view_submission`,answers:[parseAnswer({interactionId:i.requestId,text:a})],messageTs:i.messageTs,user:{id:s,username:o?.username,name:o?.name}};return t.waitUntil(dispatchViewAnswer({ctx:t,deps:n,installationTeamId:l,metadata:i,submission:u,teamId:c,text:a})),r}async function dispatchViewAnswer(e){let t=await authorizeAnswer({channelId:e.metadata.channelId,deps:e.deps,installationTeamId:e.installationTeamId,submission:e.submission,teamId:e.teamId,threadTs:e.metadata.threadTs});if(t===null)return;let n=e.metadata.messageChannelId??e.metadata.channelId,r=await respondOrRetireInactivePrompt(()=>e.ctx.from(e.metadata.continuationToken).respond(e.submission.answers,{auth:t.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleFreeformCard({card:{label:t,text:t},channelId:n,deps:e.deps,installationTeamId:e.installationTeamId,messageTs:e.metadata.messageTs})},log);if(r.outcome===`inactive`&&await Promise.all([r.retirement,postEphemeralNotice({channelId:n,deps:e.deps,installationTeamId:e.installationTeamId,teamId:e.teamId??void 0,threadTs:n===e.metadata.channelId?e.metadata.threadTs:``,userId:e.submission.user.id})]),r.outcome===`delivered`)try{await settleFreeformCard({card:{label:e.text,text:`Answered: ${e.text}`,userId:e.submission.user.id},channelId:n,deps:e.deps,installationTeamId:e.installationTeamId,messageTs:e.metadata.messageTs})}catch(e){log.error(`freeform answered-card update failed`,{error:e})}}export{handleInteractionPost,parseBlockActionsPayload,parseShortcutPayload};
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{parseAnswer}from"#shared/input.js";import{INACTIVE_REQUEST_NOTICE,respondOrRetireInactivePrompt}from"#channel/inactive-prompt.js";import{buildSlackBinding,buildSlackWorkspaceHandle,callSlackApi,resolveSlackBotToken,slackCodeChannelContinuationToken,slackContinuationToken,slackSessionToken}from"#public/channels/slack/api.js";import{buildSlackAuthContext}from"#public/channels/slack/auth.js";import{readSlackTextObject}from"#public/channels/slack/inbound-content.js";import{isSlackCodeChannel}from"#public/channels/slack/code-channel.js";import{isSlackResponseError}from"#public/channels/slack/transport.js";import{dispatchSignInCancel}from"#public/channels/slack/sign-in-cancel.js";import{HITL_FREEFORM_MODAL_ACTION_ID,HITL_FREEFORM_MODAL_BLOCK_ID,HITL_FREEFORM_MODAL_CALLBACK_ID,buildFreeformModalView,decodeFreeformHitlActionId,decodeHitlResponse,isFreeformAction,isHitlAction}from"#public/channels/slack/hitl.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{settleFreeformCard,settleHitlCard}from"#public/channels/slack/interaction-cards.js";import{authorizeAnswer}from"#public/channels/slack/answers.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{dispatchSlashCommand}from"#public/channels/slack/slash-command.js";const log=createLogger(`slack.interactions`);function parseBlockActionsPayload(e){if(isSharedBlockActionsPayload(e))return parseSharedBlockActionsPayload(e);let t=e,n=t.actions;if(!Array.isArray(n))return null;let r=t.channel?.id,i=t.message,a=i?.thread_ts??i?.ts;if(!r||!a)return null;let o=t.team,s=t.user,c=s.team_id??o?.id,l={id:s.id,username:s.username,name:s.name},u=i?.blocks??[];return{actions:n.map(e=>({actionId:String(e.action_id??``),value:e.value==null?void 0:String(e.value),blockId:e.block_id==null?void 0:String(e.block_id),selectedOptionValue:extractSelectedOptionValue(e),messageTs:i?.ts,triggerId:typeof t.trigger_id==`string`?t.trigger_id:void 0,label:extractActionLabel(e),user:l})),channelId:r,installationTeamId:readInstallationTeamId(t),threadTs:a,teamId:c,messageBlocks:u}}function isSharedBlockActionsPayload(e){return e.kind===`block_actions`&&Array.isArray(e.actions)}function parseSharedBlockActionsPayload(e){return!e.channelId||!e.threadTs?null:{actions:e.actions.map(t=>({actionId:t.actionId,value:t.value,blockId:t.blockId,selectedOptionValue:t.selectedOptionValue,messageTs:e.messageTs,triggerId:e.triggerId,label:t.label,user:{id:t.user?.id??e.userId,username:t.user?.username,name:t.user?.name}})),channelId:e.channelId,installationTeamId:readInstallationTeamId(e.raw),threadTs:e.threadTs,teamId:e.user?.teamId??e.teamId,messageBlocks:e.messageBlocks??[]}}function extractSelectedOptionValue(e){let t=e.selected_option;return typeof t?.value==`string`?t.value:void 0}function extractActionLabel(e){let t=e.selected_option?.text?.text;if(typeof t==`string`&&t.length>0)return t;let n=e.text?.text;if(typeof n==`string`&&n.length>0)return n}function findPromptBlocks(e){let t=[];for(let n of e){if(typeof n!=`object`||!n)continue;let e=n.type;if(e===`actions`)break;(e===`section`||e===`context`||e===`divider`||e===`image`)&&t.push(n)}return t}function readPromptTextFromBlocks(e){let t=findPromptBlocks(e)[0],n=readSlackTextObject(t?.text);return n.length>0?n:void 0}function isObjectRecord(e){return typeof e==`object`&&!!e}function readInstallationTeamId(e){if(!isObjectRecord(e))return;let t=isObjectRecord(e.view)?e.view:void 0,n=isObjectRecord(e.team)?e.team:void 0,r=isObjectRecord(e.user)?e.user:void 0;return[t?.app_installed_team_id,e.app_installed_team_id,n?.id,r?.team_id,t?.team_id].find(e=>typeof e==`string`&&e.length>0)}async function handleInteractionPost(e,t,n){let r=new Response(`ok`,{status:200}),i;try{i=parseSlackWebhookBody(e,{contentType:`application/x-www-form-urlencoded`})}catch{return log.warn(`failed to parse Slack interaction payload`),r}if(i.kind===`view_submission`)return handleViewSubmission(i,t,n);if(i.kind===`slash_command`)return dispatchSlashCommand(i,t,n),new Response(null,{status:200});if(i.kind===`unsupported`){let e=parseShortcutPayload(i.raw);return e===null?(log.warn(`unsupported Slack interaction payload ignored`,{type:i.type}),r):(dispatchShortcut(e,readInstallationTeamId(i.raw),t,n),new Response(null,{status:200}))}if(i.kind!==`block_actions`)return log.warn(`unsupported Slack interaction payload ignored`,{type:i.kind}),r;if(dispatchSignInCancel(i.raw,t))return r;let a=parseBlockActionsPayload(i);if(!a)return r;let o=a.actions.find(e=>isFreeformAction(e.actionId));if(o)return await openFreeformModal({payload:i.raw,interaction:a,freeformAction:o,deps:n}),r;let s=a.actions.flatMap(e=>{let t=decodeHitlResponse(e);return t===null?[]:[{action:e,derived:t}]});if(s.length>0){let e=s[0].action.user,r=s[0].derived.route;t.waitUntil(dispatchBlockAnswers({ctx:t,deps:n,interaction:a,route:r,submission:{type:`block_actions`,actions:s.map(({action:e})=>e),answers:s.map(({derived:e})=>e.response),messageTs:s[0].action.messageTs,user:e}}))}let c=n.config.onInteraction;if(c){let e=a.actions.filter(e=>!isHitlAction(e.actionId));e.length>0&&t.waitUntil(dispatchCustomInteractions({ctx:t,customActions:e,deps:n,interaction:a,onInteraction:c}).catch(e=>{log.error(`custom interaction dispatch failed`,{error:e})}))}return r}async function dispatchCustomInteractions(e){let{ctx:t,customActions:n,deps:r,interaction:i,onInteraction:a}=e,o=n[0].user,s=r.config.codeChannels===!0&&await isSlackCodeChannel(i.channelId,buildSlackWorkspaceHandle({api:r.api,botToken:r.config.credentials?.botToken,installationTeamId:i.installationTeamId,teamId:i.teamId}).request),c=s?``:i.threadTs,l=s&&i.threadTs!==n[0].messageTs?i.threadTs:void 0,{thread:u,slack:d}=buildSlackBinding({api:r.api,botToken:r.config.credentials?.botToken,channelId:i.channelId,threadTs:c,codeChannel:s,replyThreadTs:()=>l,installationTeamId:i.installationTeamId,teamId:i.teamId}),f={...bindSlackSessionOperations({address:s?slackCodeChannelContinuationToken(i.channelId):slackContinuationToken({channelId:i.channelId,threadTs:c}),defaultAuth:buildSlackAuthContext({channelId:i.channelId,installationTeamId:i.installationTeamId,teamId:i.teamId,threadTs:c,userId:o.id,userName:o.username??o.name}),from:t.from,resolveSession:t.resolveSession,state:{channelId:i.channelId,installationTeamId:i.installationTeamId??null,teamId:i.teamId??null,threadTs:c||null,triggeringUserId:o.id,...s&&{codeChannel:!0,replyThreadTs:l??null}}}),thread:u,slack:d};await Promise.all(n.map(e=>Promise.resolve().then(()=>a(e,f)).catch(e=>{log.error(`custom interaction handler failed`,{error:e})})))}function parseShortcutPayload(e){if(!isObjectRecord(e))return null;let t=e.type;if(t!==`message_action`&&t!==`shortcut`)return null;let n=readRequiredString(e.callback_id),r=readRequiredString(e.trigger_id),i=isObjectRecord(e.user)?e.user:void 0,a=readRequiredString(i?.id);if(n===null||r===null||a===null)return null;let o=isObjectRecord(e.team)?e.team:void 0,s=readOptionalString(i?.team_id)??readOptionalString(o?.id),c={id:a,username:readOptionalString(i?.username),name:readOptionalString(i?.name)};if(t===`shortcut`)return{type:t,callbackId:n,triggerId:r,user:c,teamId:s};let l=isObjectRecord(e.channel)?e.channel:void 0,u=isObjectRecord(e.message)?e.message:void 0,d=readRequiredString(l?.id),f=readRequiredString(u?.ts);return d===null||f===null||u===void 0?null:{type:t,callbackId:n,triggerId:r,user:c,teamId:s,channelId:d,message:{text:typeof u.text==`string`?u.text:``,ts:f,threadTs:readOptionalString(u.thread_ts),userId:readOptionalString(u.user)},responseUrl:readOptionalString(e.response_url)}}function readRequiredString(e){return typeof e==`string`&&e.length>0?e:null}function readOptionalString(e){return typeof e==`string`&&e.length>0?e:void 0}function dispatchShortcut(e,t,n,r){let i=r.config.onShortcut;if(i===void 0){log.warn(`Slack shortcut ignored because onShortcut is not configured`,{type:e.type});return}let a={slack:buildSlackWorkspaceHandle({api:r.api,botToken:r.config.credentials?.botToken,installationTeamId:t,teamId:e.teamId})};dispatchInteractionHook(()=>i(e,a),n,`shortcut handler failed`)}function dispatchInteractionHook(e,t,n){t.waitUntil(Promise.resolve().then(e).catch(e=>{log.error(n,{error:e})}))}async function dispatchBlockAnswers(e){let t=e.route??{channelId:e.interaction.channelId,threadTs:e.interaction.threadTs},n=await authorizeAnswer({deps:e.deps,installationTeamId:e.interaction.installationTeamId,submission:e.submission,target:t,teamId:e.interaction.teamId});if(n===null)return;let r=await respondOrRetireInactivePrompt(()=>e.ctx.from(slackSessionToken(t)).respond(e.submission.answers,{auth:n.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleHitlCard(e.interaction,e.deps,{label:t,text:t})},log);r.outcome===`inactive`&&await Promise.all([r.retirement,postEphemeralNotice({channelId:e.interaction.channelId,deps:e.deps,installationTeamId:e.interaction.installationTeamId,teamId:e.interaction.teamId,threadTs:e.interaction.threadTs,userId:e.submission.user.id})])}async function postEphemeralNotice(e){let{thread:t}=buildSlackBinding({api:e.deps.api,botToken:e.deps.config.credentials?.botToken,channelId:e.channelId,installationTeamId:e.installationTeamId,teamId:e.teamId,threadTs:e.threadTs});try{await t.postEphemeral(e.userId,INACTIVE_REQUEST_NOTICE)}catch(e){log.error(`inactive prompt notice failed`,{error:e})}}async function openFreeformModal(e){let t=e.payload.trigger_id;if(typeof t!=`string`||t.length===0){log.warn(`freeform button click missing trigger_id — cannot open modal`);return}let n=decodeFreeformHitlActionId(e.freeformAction.actionId);if(n===null){log.warn(`freeform button click carries invalid metadata`);return}let r=n.requestId;if(!r){log.warn(`freeform button click missing requestId`);return}let i=e.freeformAction.messageTs;if(!i){log.warn(`freeform button click missing messageTs`);return}let a=n.route??{channelId:e.interaction.channelId,threadTs:e.interaction.threadTs},o={...a,continuationToken:slackSessionToken(a),installationTeamId:e.interaction.installationTeamId,messageChannelId:e.interaction.channelId,messageTs:i,requestId:r},s=readPromptTextFromBlocks(e.interaction.messageBlocks),c=buildFreeformModalView({metadata:o,prompt:s}),l=await resolveSlackBotToken(e.deps.config.credentials?.botToken,{teamId:e.interaction.installationTeamId});try{await callSlackApi({api:e.deps.api,botToken:l,operation:`views.open`,body:{trigger_id:t,view:c}})}catch(e){if(!isSlackResponseError(e))throw e;log.error(`Slack views.open failed`,{error:e})}}async function handleViewSubmission(e,t,n){let r=new Response(null,{status:200});if(e.callbackId!==HITL_FREEFORM_MODAL_CALLBACK_ID)return r;let i;try{i=JSON.parse(e.privateMetadata??``)}catch{return log.warn(`freeform view_submission carries invalid private_metadata`),r}if(!i.continuationToken||!i.requestId||!i.messageTs||!i.channelId||i.codeChannel!==!0&&!i.threadTs)return r;let a=e.values?.find(e=>e.blockId===HITL_FREEFORM_MODAL_BLOCK_ID&&e.actionId===HITL_FREEFORM_MODAL_ACTION_ID)?.value??``;if(a.length===0)return r;let o=e.user,s=e.userId,c=o?.teamId??e.teamId??null,l=i.installationTeamId??readInstallationTeamId(e.raw),u={type:`view_submission`,answers:[parseAnswer({interactionId:i.requestId,text:a})],messageTs:i.messageTs,user:{id:s,username:o?.username,name:o?.name}};return t.waitUntil(dispatchViewAnswer({ctx:t,deps:n,installationTeamId:l,metadata:i,submission:u,teamId:c})),r}async function dispatchViewAnswer(e){let{channelId:t,threadTs:n}=e.metadata,r=await authorizeAnswer({deps:e.deps,installationTeamId:e.installationTeamId,submission:e.submission,target:e.metadata,teamId:e.teamId});if(r===null)return;let i=e.metadata.messageChannelId??t,a=await respondOrRetireInactivePrompt(()=>e.ctx.from(e.metadata.continuationToken).respond(e.submission.answers,{auth:r.auth,state:withSlackResponder(void 0,e.submission.user.id)}),{retirePrompt:t=>settleFreeformCard({card:{label:t,text:t},channelId:i,deps:e.deps,installationTeamId:e.installationTeamId,messageTs:e.metadata.messageTs})},log);a.outcome===`inactive`&&await Promise.all([a.retirement,postEphemeralNotice({channelId:i,deps:e.deps,installationTeamId:e.installationTeamId,teamId:e.teamId??void 0,threadTs:i===t?n??``:``,userId:e.submission.user.id})])}export{handleInteractionPost,parseBlockActionsPayload,parseShortcutPayload};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ToolContext } from "#tools/definition.js";
|
|
2
|
+
import type { SlackChannel, SlackOpenCodeChannelOptions } from "./slackChannel.js";
|
|
3
|
+
/** What {@link openCodeChannel} returns to the tool that calls it. */
|
|
4
|
+
export type SlackCodeChannelOpenResult = {
|
|
5
|
+
readonly sessionId: string;
|
|
6
|
+
readonly status: "opened";
|
|
7
|
+
} | {
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
readonly status: "unavailable";
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Forks the Slack conversation that called a tool into a new Slack code
|
|
13
|
+
* channel. The code channel's session starts with the conversation the model
|
|
14
|
+
* saw, then `message`. Slack links the channel in the conversation, and people
|
|
15
|
+
* continue the work there.
|
|
16
|
+
*
|
|
17
|
+
* Call it from your own tool, passing your Slack channel, which needs
|
|
18
|
+
* `codeChannels: true`:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* async execute({ message, name }, ctx) {
|
|
22
|
+
* return await openCodeChannel(ctx, slack, message, { name });
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Returns `unavailable` when the conversation isn't a Slack message or is
|
|
27
|
+
* already a code channel. When Slack refuses, it throws with Slack's error. A
|
|
28
|
+
* retried call reaches the same code channel and session once.
|
|
29
|
+
*/
|
|
30
|
+
export declare function openCodeChannel(ctx: Pick<ToolContext, "callId" | "channel" | "messages" | "session" | "to">, slack: SlackChannel, message: string, options?: SlackOpenCodeChannelOptions): Promise<SlackCodeChannelOpenResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
async function openCodeChannel(e,t,n,r={}){let{channelId:i,codeChannel:a,installationTeamId:o,threadTs:s,triggeringMessageTs:c}=e.channel.metadata??{};return a===!0?unavailable(`This conversation is already a code channel.`):typeof i!=`string`||typeof s!=`string`?unavailable(`This conversation is not a Slack message.`):{sessionId:(await e.to(t,{channelId:i,codeChannel:{...r.name!==void 0&&{name:r.name},originThreadTs:s,originTs:typeof c==`string`?c:s},...typeof o==`string`&&{installationTeamId:o}}).send(n,{auth:e.session.auth.current,history:textHistory(e.messages),idempotencyKey:`${e.session.id}:${e.callId}`})).sessionId,status:`opened`}}function unavailable(e){return{reason:e,status:`unavailable`}}function textHistory(e){return e.flatMap(e=>{if(e.role!==`user`&&e.role!==`assistant`)return[];let t=typeof e.content==`string`?e.content:e.content.flatMap(e=>e.type===`text`?[e.text]:[]).join(``);return t===``?[]:[{role:e.role,text:t}]})}export{openCodeChannel};
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import type { SlackHandle } from "#public/channels/slack/api.js";
|
|
2
|
-
import type {
|
|
3
|
-
import type
|
|
2
|
+
import type { BlockKitBlock } from "#public/channels/slack/blocks.js";
|
|
3
|
+
import { type RenderedRequestCard } from "#public/channels/slack/request-card-render.js";
|
|
4
4
|
type PrivateApprovalSlack = Pick<SlackHandle, "channelId" | "request" | "threadTs">;
|
|
5
|
+
/** Where a direct-message request's messages landed. */
|
|
6
|
+
export interface PrivateInteractionDelivery {
|
|
7
|
+
readonly channelId: string;
|
|
8
|
+
readonly ts: string;
|
|
9
|
+
readonly detailsTs?: string;
|
|
10
|
+
/** The controls blocks posted: the rendered ones, or eve's default if Slack rejected them. */
|
|
11
|
+
readonly blocks: readonly BlockKitBlock[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Posts one rendered request to `reviewer`'s DM: a permalink to the thread, its details, then
|
|
15
|
+
* its controls. A failure deletes what it posted, so a reviewer never sees half a request.
|
|
16
|
+
*/
|
|
5
17
|
export declare function deliverPrivateInteraction(input: {
|
|
6
18
|
readonly previewMessageTs: string;
|
|
7
|
-
readonly
|
|
19
|
+
readonly card: RenderedRequestCard;
|
|
8
20
|
readonly reviewer: string;
|
|
9
21
|
readonly slack: PrivateApprovalSlack;
|
|
10
|
-
}): Promise<
|
|
22
|
+
}): Promise<PrivateInteractionDelivery>;
|
|
11
23
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger,logError}from"#internal/logging.js";import{
|
|
1
|
+
import{createLogger,logError}from"#internal/logging.js";import{sendWithFallback}from"#public/channels/slack/request-card-render.js";const log=createLogger(`slack.private-approval-delivery`);async function deliverPrivateInteraction(e){let t=await e.slack.request(`conversations.open`,{users:e.reviewer}),n=t.ok===!0?t.channel?.id:void 0;if(typeof n!=`string`||n.length===0)throw Error(`Slack conversations.open failed: ${t.error??`unknown_error`}`);let{fallback:r,parts:i}=e.card,a=[],post=async t=>{let r=await postMessage(e.slack,{channel:n,unfurl_links:!1,unfurl_media:!1,...t});return a.push(r),r},postPart=async e=>{let t=await post({blocks:e.blocks,text:e.text});return{blocks:e.blocks,ts:t}};try{let t=await resolveMessagePermalink(e.slack,e.previewMessageTs);t!==void 0&&await post({markdown_text:t,unfurl_links:!0});let a=i.details===void 0?void 0:await sendWithFallback(postPart,i.details,r.details),o=await sendWithFallback(postPart,i.controls,r.controls),s={blocks:o.blocks,channelId:n,ts:o.ts};return a!==void 0&&(s.detailsTs=a.ts),s}catch(t){throw await Promise.allSettled(a.map(async t=>{try{await e.slack.request(`chat.delete`,{channel:n,ts:t})}catch(e){logError(log,`failed to roll back partial private approval delivery`,e,{channelId:n,messageTs:t})}})),t}}async function resolveMessagePermalink(e,t){if(!e.channelId||!t)return;let n=await e.request(`chat.getPermalink`,{channel:e.channelId,message_ts:t});return n.ok===!0&&typeof n.permalink==`string`?n.permalink:void 0}async function postMessage(e,t){let n=await e.request(`chat.postMessage`,t);if(n.ok!==!0||typeof n.ts!=`string`||n.ts.length===0)throw Object.assign(Error(`Slack chat.postMessage failed: ${n.error??`unknown_error`}`),{response:n});return n.ts}export{deliverPrivateInteraction};
|
|
@@ -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};
|