eve 0.70.1 → 0.70.2
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 +24 -0
- package/dist/src/channel/adapter.js +1 -1
- package/dist/src/channel/channel-operations.js +1 -1
- package/dist/src/channel/resolve-text.d.ts +8 -0
- package/dist/src/channel/resolve-text.js +2 -1
- package/dist/src/cli/banner.d.ts +2 -2
- package/dist/src/cli/banner.js +1 -1
- package/dist/src/cli/commands/init-agent-workspace.js +1 -1
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/dev/tui/runner.d.ts +2 -1
- package/dist/src/cli/dev/tui/runner.js +1 -1
- package/dist/src/cli/dev/tui/terminal-renderer.js +2 -2
- package/dist/src/cli/dev/tui/test/mock-terminal.d.ts +7 -1
- package/dist/src/cli/dev/tui/test/mock-terminal.js +4 -3
- package/dist/src/cli/dev/tui/theme.d.ts +1 -1
- package/dist/src/cli/dev/tui/theme.js +1 -1
- package/dist/src/cli/dev/tui/tool-presentation.d.ts +15 -2
- package/dist/src/cli/dev/tui/tool-presentation.js +1 -1
- package/dist/src/cli/dev/tui/transcript-parts.js +1 -1
- package/dist/src/compiled/.vendor-stamp.json +7 -7
- package/dist/src/compiled/@photon-ai/chat-adapter-imessage/index.js +1 -1
- package/dist/src/compiled/@vercel/sandbox/index.js +1 -1
- package/dist/src/compiled/@workflow/core/index.js +2 -2
- package/dist/src/compiled/@workflow/core/runtime/constants.d.ts +15 -0
- package/dist/src/compiled/@workflow/core/runtime/dynamic-workflow.d.ts +182 -0
- package/dist/src/compiled/@workflow/core/runtime/helpers.d.ts +7 -0
- package/dist/src/compiled/@workflow/core/runtime/helpers.js +1 -1
- package/dist/src/compiled/@workflow/core/runtime/quickjs-assets.generated.d.ts +7 -0
- package/dist/src/compiled/@workflow/core/runtime/quickjs-entrypoint.d.ts +2 -30
- package/dist/src/compiled/@workflow/core/runtime/quickjs-log-view.d.ts +27 -4
- package/dist/src/compiled/@workflow/core/runtime/quickjs-runtime.d.ts +55 -3
- package/dist/src/compiled/@workflow/core/runtime/quickjs-snapshot-codec.d.ts +74 -0
- package/dist/src/compiled/@workflow/core/runtime/start.d.ts +4 -0
- package/dist/src/compiled/@workflow/core/runtime/vm-mode.d.ts +44 -0
- package/dist/src/compiled/@workflow/core/runtime.d.ts +1 -1
- package/dist/src/compiled/@workflow/core/runtime.js +4 -4
- package/dist/src/compiled/@workflow/core/schemas.d.ts +1 -1
- package/dist/src/compiled/@workflow/core/serialization/compression.d.ts +27 -2
- package/dist/src/compiled/@workflow/core/serialization/hardened.d.ts +8 -0
- package/dist/src/compiled/@workflow/core/serialization/types.d.ts +1 -0
- package/dist/src/compiled/@workflow/core/serialization.d.ts +48 -0
- package/dist/src/compiled/@workflow/core/serialization.js +1 -1
- package/dist/src/compiled/@workflow/core/set-error-stack.d.ts +12 -0
- package/dist/src/compiled/@workflow/core/telemetry/semantic-conventions.d.ts +47 -0
- package/dist/src/compiled/@workflow/core/version.d.ts +1 -1
- package/dist/src/compiled/@workflow/core/workflow.js +1 -1
- package/dist/src/compiled/@workflow/world/events.d.ts +10 -0
- package/dist/src/compiled/@workflow/world/index.d.ts +2 -0
- package/dist/src/compiled/@workflow/world/interfaces.d.ts +127 -0
- package/dist/src/compiled/@workflow/world/queue.d.ts +6 -0
- package/dist/src/compiled/@workflow/world/runs.d.ts +9 -2
- package/dist/src/compiled/@workflow/world/snapshots.d.ts +60 -0
- package/dist/src/compiled/@workflow/world-local/index.js +5 -5
- package/dist/src/compiled/@workflow/world-vercel/index.js +19 -17
- package/dist/src/compiled/_chunks/node/undici-B61sz6UT.js +95 -0
- package/dist/src/compiled/_chunks/workflow/{attribute-changes-D721NpZv.js → attribute-changes-VaTKT4K8.js} +3 -3
- package/dist/src/compiled/_chunks/workflow/dist-B7NC1c9-.js +1 -0
- package/dist/src/compiled/_chunks/workflow/{functions-DpZiBxgQ.js → functions-CKkqf9V6.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/helpers-CP9TjeO8.js +1 -0
- package/dist/src/compiled/_chunks/workflow/{logger-D4GJNTk4.js → logger-oqfblE3p.js} +2 -2
- package/dist/src/compiled/_chunks/workflow/resume-hook-yq-NAdBu.js +1 -0
- package/dist/src/compiled/_chunks/workflow/run-DYKeqbob.js +32 -0
- package/dist/src/compiled/_chunks/workflow/serialization-APSKphN5.js +3 -0
- package/dist/src/compiled/_chunks/workflow/signal-exit-iIdjQcCK.js +95 -0
- package/dist/src/compiled/_chunks/workflow/sleep-Ba8SExMt.js +1 -0
- package/dist/src/compiled/_chunks/workflow/{token-BhlnqoHM.js → token-BKeK6DmC.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/token-Cv2GYj9A.js +1 -0
- package/dist/src/compiled/_chunks/workflow/{token-util-DaeRTzDa.js → token-util-CddTZ_ef.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/{wait-until-3_1djUuL.js → wait-until-Dc9s3yyP.js} +1 -1
- package/dist/src/compiled/_chunks/workflow/{wrapper-CzrF_dmI.js → wrapper-Dq4OeGmJ.js} +4 -4
- package/dist/src/compiled/shadcn-registry/index.js +4 -4
- package/dist/src/context/serialize.js +1 -1
- package/dist/src/discover/discover-agent.js +1 -1
- package/dist/src/discover/extension-specifier.d.ts +1 -0
- package/dist/src/discover/extension-specifier.js +1 -1
- package/dist/src/discover/extensions.d.ts +1 -1
- package/dist/src/discover/extensions.js +1 -1
- package/dist/src/discover/project-source.d.ts +5 -0
- package/dist/src/discover/project-source.js +1 -1
- package/dist/src/evals/session.d.ts +1 -0
- package/dist/src/evals/session.js +1 -1
- package/dist/src/evals/types.d.ts +6 -0
- package/dist/src/execution/dynamic-workflow/tool.js +1 -1
- package/dist/src/execution/legacy-session/turn-workflow.js +1 -1
- package/dist/src/execution/proxied-deliver-step.d.ts +11 -0
- package/dist/src/execution/proxied-deliver-step.js +1 -1
- package/dist/src/execution/sandbox/read-file.d.ts +8 -1
- package/dist/src/execution/sandbox/read-file.js +3 -3
- package/dist/src/execution/session/active-turn.d.ts +4 -1
- package/dist/src/execution/session/active-turn.js +1 -1
- package/dist/src/execution/session/entry-input.d.ts +23 -5
- package/dist/src/execution/session/entry.js +1 -1
- package/dist/src/execution/session/handoff.d.ts +35 -13
- package/dist/src/execution/session/handoff.js +1 -1
- package/dist/src/execution/session/program.d.ts +16 -3
- package/dist/src/execution/session/program.js +1 -1
- package/dist/src/execution/session/turn-step-types.d.ts +6 -1
- package/dist/src/execution/session/turn-step.js +1 -1
- package/dist/src/execution/session/turn.js +1 -1
- package/dist/src/execution/tools/agent-router-workflow.js +1 -1
- package/dist/src/execution/workflow-runtime.d.ts +4 -4
- package/dist/src/execution/workflow-runtime.js +1 -1
- package/dist/src/harness/attachment-staging.d.ts +27 -7
- package/dist/src/harness/attachment-staging.js +1 -1
- package/dist/src/harness/compaction-prompt.js +2 -2
- package/dist/src/harness/input-requests.js +1 -1
- package/dist/src/harness/messages.js +1 -1
- package/dist/src/harness/token-estimate.d.ts +5 -3
- package/dist/src/harness/token-estimate.js +1 -1
- package/dist/src/harness/tool-loop.js +2 -2
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/attachments/media-metadata.d.ts +25 -0
- package/dist/src/internal/attachments/media-metadata.js +1 -0
- package/dist/src/internal/attachments/sandbox-refs.d.ts +12 -7
- package/dist/src/internal/attachments/sandbox-refs.js +1 -1
- package/dist/src/internal/authored-module-loader.js +1 -1
- package/dist/src/internal/bundler/extension-mount-plugin.d.ts +4 -0
- package/dist/src/internal/bundler/extension-mount-plugin.js +1 -1
- package/dist/src/internal/bundler/extension-scope-plugin.js +2 -2
- package/dist/src/internal/input-text.d.ts +12 -0
- package/dist/src/internal/input-text.js +1 -0
- package/dist/src/internal/workflow/vercel-world-defaults.d.ts +1 -1
- package/dist/src/internal/workflow/vercel-world-defaults.js +1 -1
- package/dist/src/public/channels/chat-sdk/input-actions.d.ts +8 -2
- package/dist/src/public/channels/chat-sdk/input-actions.js +4 -1
- package/dist/src/public/channels/github/defaults.js +2 -3
- package/dist/src/public/channels/linear/hitl.js +2 -3
- package/dist/src/public/channels/slack/action-status.d.ts +10 -5
- package/dist/src/public/channels/slack/action-status.js +1 -1
- package/dist/src/public/channels/slack/defaults.js +3 -3
- package/dist/src/public/channels/slack/limits.d.ts +1 -1
- package/dist/src/public/channels/slack/limits.js +1 -1
- package/dist/src/public/channels/slack/slackChannel.d.ts +11 -9
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/public/channels/slack/task-card.js +1 -1
- package/dist/src/public/channels/slack/thread-status.d.ts +33 -0
- package/dist/src/public/channels/slack/thread-status.js +1 -0
- package/dist/src/public/channels/twilio/defaults.js +4 -2
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/dist/src/subagents/hitl-proxy.d.ts +2 -2
- package/dist/src/subagents/hitl-proxy.js +1 -1
- package/dist/src/tools/provided/ask-question-workflow.js +1 -1
- package/dist/src/tools/provided/read-file.d.ts +7 -0
- package/dist/src/tools/provided/read-file.js +2 -2
- package/dist/src/tools/provided/sleep-workflow.js +1 -1
- package/docs/agent-config.md +2 -2
- package/docs/channels/chat-sdk.mdx +1 -1
- package/docs/channels/slack.mdx +21 -15
- package/docs/channels/twilio.mdx +1 -1
- package/docs/concepts/built-in-tools.md +1 -1
- package/docs/concepts/default-harness.md +4 -0
- package/docs/concepts/execution-model-and-durability.mdx +7 -1
- package/docs/concepts/sessions-runs-and-streaming.md +3 -3
- package/docs/evals/cases.mdx +1 -0
- package/docs/extensions.md +5 -3
- package/docs/guides/dev-tui.md +3 -1
- package/docs/guides/dynamic-capabilities.md +1 -1
- package/docs/sandbox/index.mdx +11 -0
- package/docs/tools/overview.mdx +4 -2
- package/package.json +7 -7
- package/dist/src/compiled/_chunks/node/undici-h5GH4RBM.js +0 -95
- package/dist/src/compiled/_chunks/workflow/debug-log-CVPstO1D.js +0 -1
- package/dist/src/compiled/_chunks/workflow/dist-BUFGAKRb.js +0 -1
- package/dist/src/compiled/_chunks/workflow/frames-Dr9fHWdo.js +0 -3
- package/dist/src/compiled/_chunks/workflow/helpers-DkKxg_yn.js +0 -1
- package/dist/src/compiled/_chunks/workflow/resume-hook-uTW-jBYz.js +0 -1
- package/dist/src/compiled/_chunks/workflow/run-5DmiMN9b.js +0 -1
- package/dist/src/compiled/_chunks/workflow/serialization-CNIYl1D3.js +0 -3
- package/dist/src/compiled/_chunks/workflow/signal-exit-DZKTacNU.js +0 -95
- package/dist/src/compiled/_chunks/workflow/sleep-DFKgN7UG.js +0 -1
- package/dist/src/compiled/_chunks/workflow/token-1wSBbi5N.js +0 -1
- package/dist/src/compiled/_chunks/workflow/token-util-Cm4Ec1Bg.js +0 -1
- package/dist/src/compiled/_chunks/workflow/ws-transport--rYOVIaf.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{actionRequestName}from"#shared/action-request-name.js";import{displayName,displayTitle}from"#shared/display-name.js";import{AGENT_TOOL_NAME}from"#tools/framework/agent-contract.js";function
|
|
1
|
+
import{actionRequestName}from"#shared/action-request-name.js";import{displayName,displayTitle}from"#shared/display-name.js";import{AGENT_TOOL_NAME}from"#tools/framework/agent-contract.js";function actionLabel(e,t){return t?.[e.callId]?.label??displayTitle(actionRequestName(e))}function withMoreCalls(e,t){return t<=1?e:`${e} +${String(t-1)} more`}function waitingOnTasks(e){let[t]=e;return e.length===1&&t!==AGENT_TOOL_NAME?`Waiting on ${displayName(t)}...`:e.length===1?`Waiting on a task...`:`Waiting on ${String(e.length)} tasks...`}function reviewingResults(e){let[t]=e;return t!=null&&t!==AGENT_TOOL_NAME&&e.every(e=>e===t)?`Reviewing ${displayName(t)}'s results...`:`Reviewing results...`}export{actionLabel,reviewingResults,waitingOnTasks,withMoreCalls};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{createLogger,extractErrorId,formatErrorHint}from"#internal/logging.js";import{isTaskControlTool}from"#protocol/task-tools.js";import{truncateTypingStatus}from"#public/channels/slack/limits.js";import{buildSlackAuthContext,slackUserIdForPrincipal}from"#public/channels/slack/auth.js";import{workingTaskNames}from"#channel/task-card.js";import{
|
|
1
|
+
import{createLogger,extractErrorId,formatErrorHint}from"#internal/logging.js";import{isTaskControlTool}from"#protocol/task-tools.js";import{truncateTypingStatus}from"#public/channels/slack/limits.js";import{buildSlackAuthContext,slackUserIdForPrincipal}from"#public/channels/slack/auth.js";import{workingTaskNames}from"#channel/task-card.js";import{actionLabel,reviewingResults,waitingOnTasks,withMoreCalls}from"#public/channels/slack/action-status.js";import{buildAuthCompletedText,buildAuthEphemeralBlocks,buildAuthEphemeralText,buildAuthRequiredPublicText,formatConnectionDisplayName}from"#public/channels/slack/connections.js";import{approvalEvents}from"#public/channels/slack/approval-cards.js";import{deliverCompletedSlackReply}from"#public/channels/slack/reply-delivery.js";import{clearStatus,hideStatus,reasoningStatus,showStatus}from"#public/channels/slack/thread-status.js";const log=createLogger(`slack.defaults`),streamByState=new WeakMap;function stepStream(e,t,n){let r=streamByState.get(e);if(r?.turnId===t&&r.stepIndex===n)return r;let i={calls:null,reasoning:``,reasoningShownAtMs:null,replyChars:0,stepIndex:n,turnId:t};return streamByState.set(e,i),i}function heldWithin(e,t){return e!=null&&t-e>=0&&t-e<3e3}async function showReasoning(e,t,n){let r=reasoningStatus(t.reasoning,n);if(r===void 0||truncateTypingStatus(r)===e.state.threadStatus?.text)return;let i=Date.now();heldWithin(t.reasoningShownAtMs,i)||(await showStatus(e,r),t.reasoningShownAtMs=i)}function extractSemanticErrorSummary(e){if(typeof e.details!=`object`||e.details===null)return null;let t=e.details;if(typeof t.semanticErrorId!=`string`||t.semanticErrorId.length===0||typeof t.name!=`string`||t.name.length===0)return null;let n=typeof t.message==`string`&&t.message.trim().length>0?t.message.trim():e.message?.trim();if(!n)return null;let r=typeof t.hint==`string`&&t.hint.trim().length>0?t.hint.trim():void 0;return r===void 0?{message:n,name:t.name}:{hint:r,message:n,name:t.name}}function formatSemanticErrorBlock(e,t){return[`### ${e.name}`,``,e.message,...e.hint?[``,`**How to fix**`,e.hint]:[],...t?[``,`**Error id:**`,`\`${t}\``]:[]].flatMap(e=>e.split(`
|
|
2
2
|
`)).map(e=>e.length>0?`> ${e}`:`> `).join(`
|
|
3
3
|
`)}function formatSemanticErrorReply(e){return[`${e.introduction}.`,``,formatSemanticErrorBlock(e.summary,e.errorId),...e.summary.hint?[]:[``,e.nextStep],...e.summary.hint&&e.followUp?[``,e.followUp]:[]].join(`
|
|
4
|
-
`)}function defaultSlackAuth(e,t){let n=e.author;return n?buildSlackAuthContext({channelId:t.slack.channelId,fullName:n.fullName,installationTeamId:e.installationTeamId,isBot:n.isBot,teamId:e.teamId,threadTs:t.slack.threadTs,userId:n.userId,userName:n.userName}):null}function defaultOnMessage(e,t){return{auth:defaultSlackAuth(t,e)}}async function defaultReceived(e,t){await t.thread.startTyping(`Thinking...`)}function firstNonEmptyLine(e){for(let t of e.split(/\r?\n/u)){let e=t.trim();if(e.length>0)return e}}const defaultEvents={...approvalEvents,async"turn.waiting"(e,t,n){let r=workingTaskNames(t.state.taskCards?.[e.turnId]?.turn);r.length>0&&await t
|
|
4
|
+
`)}function defaultSlackAuth(e,t){let n=e.author;return n?buildSlackAuthContext({channelId:t.slack.channelId,fullName:n.fullName,installationTeamId:e.installationTeamId,isBot:n.isBot,teamId:e.teamId,threadTs:t.slack.threadTs,userId:n.userId,userName:n.userName}):null}function defaultOnMessage(e,t){return{auth:defaultSlackAuth(t,e)}}async function defaultReceived(e,t){await t.thread.startTyping(`Thinking...`)}function firstNonEmptyLine(e){for(let t of e.split(/\r?\n/u)){let e=t.trim();if(e.length>0)return e}}const defaultEvents={...approvalEvents,async"turn.waiting"(e,t,n){if(e.on===`input`){await hideStatus(t);return}let r=workingTaskNames(t.state.taskCards?.[e.turnId]?.turn);r.length>0&&await showStatus(t,waitingOnTasks(r))},async"task.settled"(e,t,n){if(e.cancel!==void 0)return;let r=t.state.pendingTaskResults,i=r?.turnId===e.turnId?r.names:[],a=e.kind===`agent`&&e.name!==void 0?e.name:null;t.state.pendingTaskResults={names:[...i,a],turnId:e.turnId}},async"step.started"(e,t,n){if(e.stepIndex===0)return;let r=t.state.pendingTaskResults;t.state.pendingTaskResults=null;let i=r?.turnId===e.turnId?reviewingResults(r.names):t.state.threadStatus?.text??`Thinking...`;await showStatus(t,i,{force:!0})},async"turn.started"(e,t,n){t.state.pendingTaskResults=null,t.state.pendingToolCallMessage=null,streamByState.delete(t.state),await showStatus(t,`Thinking...`,{force:!0})},async"turn.completed"(e,t,n){await clearStatus(t)},async"reasoning.appended"(e,t,n){let r=stepStream(t.state,e.turnId,e.stepIndex);r.reasoning===``&&(r.reasoningShownAtMs=null),r.reasoning+=e.reasoningDelta,await showReasoning(t,r)},async"reasoning.completed"(e,t,n){let r=stepStream(t.state,e.turnId,e.stepIndex);r.reasoning=e.reasoning,await showReasoning(t,r,{complete:!0}),r.reasoning=``},async"message.appended"(e,t,n){let r=stepStream(t.state,e.turnId,e.stepIndex),i=r.replyChars;r.replyChars+=e.messageDelta.length,i<280&&r.replyChars>=280&&await showStatus(t,`Writing a reply...`)},async"actions.requested"(e,t,n){let r=t.state.pendingToolCallMessage;t.state.pendingToolCallMessage=null;let i=e.actions.filter(e=>e.kind!==`tool-call`||!isTaskControlTool(e.toolName)&&e.parentCallId===void 0);if(!r&&i.length===0)return;let a=stepStream(t.state,e.turnId,e.stepIndex),o=!r&&a.calls?{...a.calls,count:a.calls.count+i.length}:{count:i.length,label:r??actionLabel(i[0],e.presentation),narrated:r!=null};a.calls=o,await showStatus(t,o.narrated?o.label:withMoreCalls(o.label,o.count))},async"action.partial"(e,t,n){let r=e.presentation?.[e.result.callId]?.label;r&&!heldWithin(t.state.threadStatus?.atMs,Date.now())&&await showStatus(t,r)},async"action.result"(e,t,n){let r=e.presentation?.[e.result.callId]?.label;r&&await showStatus(t,r)},async"message.completed"(e,t,n){if(e.finishReason===`tool-calls`){t.state.pendingToolCallMessage=e.message?firstNonEmptyLine(e.message)??null:null;return}if(t.state.pendingToolCallMessage=null,!e.message){await clearStatus(t);return}t.state.threadStatus=null,await deliverCompletedSlackReply(t,e.message,{turnId:e.turnId})},async"turn.failed"(e,t,n){let r=extractErrorId(e.details),i=extractSemanticErrorSummary(e);if(i!==null){await t.thread.post(formatSemanticErrorReply({errorId:r,introduction:`I hit an error while handling your request`,nextStep:`Please try again, rephrase, or reach out if it keeps failing.`,summary:i}));return}let a=formatErrorHint(e);await t.thread.post([`I hit an error while handling your request${a}.`,``,`Please try again, rephrase, or reach out if it keeps failing.`,...r?[``,`_Error id: \`${r}\`_`]:[]].join(`
|
|
5
5
|
`))},async"session.failed"(e,t){let n=extractErrorId(e.details),r=extractSemanticErrorSummary(e);if(r!==null){await t.thread.post(formatSemanticErrorReply({errorId:n,followUp:`Start a new thread to continue — I can't pick this one back up.`,introduction:`This session couldn't recover from an error`,nextStep:`Resolve the issue, then start a new thread — I can't pick this one back up.`,summary:r}));return}let i=formatErrorHint(e);await t.thread.post([`This session couldn't recover from an error${i}.`,``,`Start a new thread to continue — I can't pick this one back up.`,...n?[``,`_Error id: \`${n}\`_`]:[]].join(`
|
|
6
|
-
`))},async"authorization.required"(e,t,n){let r=e.authorization?.displayName??formatConnectionDisplayName(e.name),i=slackUserIdForPrincipal(t.state,e.principalId)??null,a=e.authorization?.url,o=t.state.pendingAuthMessageTs??{};if(e.candidateId===void 0&&o[e.name]===void 0){let n=buildAuthRequiredPublicText({displayName:r,recipientUserId:i});try{let r=await t.thread.post(n);r.id&&(t.state.pendingAuthMessageTs={...o,[e.name]:r.id})}catch(t){log.error(`Slack auth public message delivery failed`,{name:e.name,error:t})}}if(i&&a){let{channelId:n,threadTs:o}=t.state,s={cancel:e.candidateId===void 0&&n&&o&&e.turnId?{channelId:n,threadTs:o,turnId:e.turnId}:void 0,displayName:r,url:a,userCode:e.authorization?.userCode};try{await t.thread.postEphemeral(i,{blocks:buildAuthEphemeralBlocks(s),text:buildAuthEphemeralText(s)})}catch(t){log.error(`Slack auth ephemeral delivery failed`,{name:e.name,error:t})}}},async"authorization.completed"(e,t,n){let r=e.authorization?.displayName??formatConnectionDisplayName(e.name);e.outcome===`authorized`&&e.candidateId===void 0&&await t
|
|
6
|
+
`))},async"authorization.required"(e,t,n){let r=e.authorization?.displayName??formatConnectionDisplayName(e.name),i=slackUserIdForPrincipal(t.state,e.principalId)??null,a=e.authorization?.url,o=t.state.pendingAuthMessageTs??{};if(e.candidateId===void 0&&o[e.name]===void 0){let n=buildAuthRequiredPublicText({displayName:r,recipientUserId:i});try{let r=await t.thread.post(n);r.id&&(t.state.pendingAuthMessageTs={...o,[e.name]:r.id})}catch(t){log.error(`Slack auth public message delivery failed`,{name:e.name,error:t})}}if(i&&a){let{channelId:n,threadTs:o}=t.state,s={cancel:e.candidateId===void 0&&n&&o&&e.turnId?{channelId:n,threadTs:o,turnId:e.turnId}:void 0,displayName:r,url:a,userCode:e.authorization?.userCode};try{await t.thread.postEphemeral(i,{blocks:buildAuthEphemeralBlocks(s),text:buildAuthEphemeralText(s)})}catch(t){log.error(`Slack auth ephemeral delivery failed`,{name:e.name,error:t})}}},async"authorization.completed"(e,t,n){let r=e.authorization?.displayName??formatConnectionDisplayName(e.name);e.outcome===`authorized`&&e.candidateId===void 0&&await showStatus(t,`Connected to ${r}. Resuming...`,{force:!0});let i=t.state.pendingAuthMessageTs??{},a=i[e.name];if(a===void 0)return;let o=buildAuthCompletedText({displayName:r,outcome:e.outcome,reason:e.reason});try{await t.slack.request(`chat.update`,{channel:t.slack.channelId,ts:a,text:o})}catch(t){log.error(`Slack auth status edit failed`,{name:e.name,error:t})}let s={...i};delete s[e.name],t.state.pendingAuthMessageTs=s}};export{defaultEvents,defaultOnMessage,defaultReceived,defaultSlackAuth};
|
|
@@ -59,7 +59,7 @@ export declare const SLACK_MODAL_TITLE_MAX_LENGTH = 24;
|
|
|
59
59
|
* assistant-thread status does not render, trims, collapses runs of
|
|
60
60
|
* whitespace into a single space, then truncates to
|
|
61
61
|
* {@link SLACK_TYPING_STATUS_MAX_LENGTH} with a trailing ellipsis when
|
|
62
|
-
* needed.
|
|
62
|
+
* needed, at a word boundary when one is near.
|
|
63
63
|
*/
|
|
64
64
|
export declare function truncateTypingStatus(status: string): string;
|
|
65
65
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
const SLACK_TYPING_STATUS_MAX_LENGTH=50,SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH=75,SLACK_SECTION_TEXT_MAX_LENGTH=3e3,SLACK_CARD_BODY_TEXT_MAX_LENGTH=200,SLACK_CARD_SUBTEXT_MAX_LENGTH=200,SLACK_MESSAGE_TEXT_MAX_LENGTH=4e4,SLACK_MARKDOWN_TEXT_MAX_LENGTH=12e3,SLACK_MAX_BLOCKS_PER_MESSAGE=50,SLACK_MODAL_TITLE_MAX_LENGTH=24;function truncateTypingStatus(e){
|
|
1
|
+
const SLACK_TYPING_STATUS_MAX_LENGTH=50,SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH=75,SLACK_SECTION_TEXT_MAX_LENGTH=3e3,SLACK_CARD_BODY_TEXT_MAX_LENGTH=200,SLACK_CARD_SUBTEXT_MAX_LENGTH=200,SLACK_MESSAGE_TEXT_MAX_LENGTH=4e4,SLACK_MARKDOWN_TEXT_MAX_LENGTH=12e3,SLACK_MAX_BLOCKS_PER_MESSAGE=50,SLACK_MODAL_TITLE_MAX_LENGTH=24;function truncateTypingStatus(e){let t=stripTypingStatusMarkdown(e).trim().replace(/\s+/gu,` `);if(t.length<=50)return t;let n=t.slice(0,47),r=n.lastIndexOf(` `);return`${(r>25?n.slice(0,r):n).trimEnd()}...`}function truncatePlainText(e){if(e!==void 0)return truncateWithEllipsis(e,75)}function truncateSectionText(e){return truncateWithEllipsis(e,SLACK_SECTION_TEXT_MAX_LENGTH)}function truncateCardBodyText(e){return truncateWithEllipsis(e,200)}function truncateCardSubtext(e){return truncateWithEllipsis(e,200)}function truncateMessageText(e){return truncateWithEllipsis(e,SLACK_MESSAGE_TEXT_MAX_LENGTH)}function truncateModalTitle(e){return truncateWithEllipsis(e,24)}function truncateWithEllipsis(e,t){if(e.length<=t)return e;let n=Math.max(0,t-3);return`${e.slice(0,n).trimEnd()}...`}function stripTypingStatusMarkdown(e){return e.replace(/\[([^\]]+)\]\([^)]+\)/gu,`$1`).replace(/`([^`]+)`/gu,`$1`).replace(/~~([^~]+)~~/gu,`$1`).replace(/(^|[^\p{L}\p{N}])(\*\*|__)([^*_]+)\2(?=$|[^\p{L}\p{N}])/gu,`$1$3`).replace(/(^|[^\p{L}\p{N}*])\*([^*_]+)\*(?=$|[^\p{L}\p{N}*])/gu,`$1$2`).replace(/(^|[^\p{L}\p{N}_])_([^*_]+)_(?=$|[^\p{L}\p{N}_])/gu,`$1$2`)}export{SLACK_BLOCK_KIT_PLAIN_TEXT_MAX_LENGTH,SLACK_CARD_BODY_TEXT_MAX_LENGTH,SLACK_CARD_SUBTEXT_MAX_LENGTH,SLACK_MARKDOWN_TEXT_MAX_LENGTH,SLACK_MAX_BLOCKS_PER_MESSAGE,SLACK_MESSAGE_TEXT_MAX_LENGTH,SLACK_MODAL_TITLE_MAX_LENGTH,SLACK_SECTION_TEXT_MAX_LENGTH,SLACK_TYPING_STATUS_MAX_LENGTH,truncateCardBodyText,truncateCardSubtext,truncateMessageText,truncateModalTitle,truncatePlainText,truncateSectionText,truncateTypingStatus};
|
|
@@ -12,6 +12,7 @@ import { type SlackBotToken, type SlackHandle, type SlackThread, type SlackWorks
|
|
|
12
12
|
import { type SlackTransportOptions } from "#public/channels/slack/transport.js";
|
|
13
13
|
import { type SlackRenderedEvent, type SlackRenderer } from "#public/channels/slack/renderers.js";
|
|
14
14
|
import { type SlackTaskCardState } from "#public/channels/slack/task-card.js";
|
|
15
|
+
import type { SlackThreadStatus } from "#public/channels/slack/thread-status.js";
|
|
15
16
|
import { type SlackEvent, type SlackEventEnvelope, type SlackMessage } from "#public/channels/slack/inbound.js";
|
|
16
17
|
import { type LoadThreadContextMessagesOptions } from "#public/channels/slack/thread.js";
|
|
17
18
|
import { type SlackSessionOperations } from "#public/channels/slack/session-operations.js";
|
|
@@ -127,12 +128,10 @@ export interface SlackChannelState {
|
|
|
127
128
|
*/
|
|
128
129
|
pendingToolCallMessage?: string | null;
|
|
129
130
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* extensions immediately while throttling smaller streamed deltas.
|
|
131
|
+
* The thread status the default renderer last set. A later model step shows
|
|
132
|
+
* it again, as does a new task card, which Slack clears the status for.
|
|
133
133
|
*/
|
|
134
|
-
|
|
135
|
-
lastReasoningTypingStatus?: string | null;
|
|
134
|
+
threadStatus?: SlackThreadStatus | null;
|
|
136
135
|
/**
|
|
137
136
|
* Connection name to Slack message ts. Each entry is the public
|
|
138
137
|
* link-free status post created by the default
|
|
@@ -148,11 +147,14 @@ export interface SlackChannelState {
|
|
|
148
147
|
*/
|
|
149
148
|
taskCards?: Record<string, SlackTaskCardState> | null;
|
|
150
149
|
/**
|
|
151
|
-
*
|
|
152
|
-
* `
|
|
153
|
-
* the
|
|
150
|
+
* Tasks that settled since the turn's last model step: each named agent, or
|
|
151
|
+
* `null` for another task. The default `step.started` handler shows
|
|
152
|
+
* `Reviewing results...` for the step that reads them.
|
|
154
153
|
*/
|
|
155
|
-
|
|
154
|
+
pendingTaskResults?: {
|
|
155
|
+
readonly names: readonly (string | null)[];
|
|
156
|
+
readonly turnId: string;
|
|
157
|
+
} | null;
|
|
156
158
|
/**
|
|
157
159
|
* Principal id to Slack user id, recorded as each message or input response
|
|
158
160
|
* is delivered. Default handlers use it to address the principal named on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{markEventHandled}from"./utils.js";import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{setChannelBuildMetadata}from"#channel/compiled-channel.js";import{defaultDeliverResult}from"#channel/adapter.js";import{POST,defineChannel}from"#public/definitions/channel.js";import{mergeUploadPolicy}from"#public/channels/upload-policy.js";import{resolveSlackTransportOptions}from"#public/channels/slack/transport.js";import{buildSlackAuthContext,slackUserIdFromAuthContext}from"#public/channels/slack/auth.js";import{buildSlackBinding,buildSlackWorkspaceHandle,slackContinuationToken}from"#public/channels/slack/api.js";import{defaultInputRequestedHandler}from"#public/channels/slack/approval-cards.js";import{withFinalReplyDelivery,withUndeliveredReplyNote}from"#public/channels/slack/reply-delivery.js";import{defaultEvents,defaultOnMessage,defaultReceived}from"#public/channels/slack/defaults.js";import{composeSlackRenderers}from"#public/channels/slack/renderers.js";import{loadThreadContextMessages}from"#public/channels/slack/thread.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{buildSlackTurnMessage,collectInboundFileParts,createSlackFetchFile}from"#public/channels/slack/attachments.js";import{renderDefaultSlackTaskCard,withTaskCards}from"#public/channels/slack/task-card.js";import{parseMessageEvent,parseSlackEventEnvelope,slackEventBotUserId,slackEventInstallationTeamId,slackEventReceivingBotUserId,slackMessageFromWebhookPayload}from"#public/channels/slack/inbound.js";import{formatSlackInboundMessage,formatSlackThreadContext}from"#public/channels/slack/model-context.js";import{isPrivateSlackConversation}from"#public/channels/slack/privacy.js";import{SLACK_CHANNEL_DEFAULT_ROUTE}from"#public/channels/slack/constants.js";import{defineSlackAppManifest}from"#public/channels/slack/app-manifest.js";import{handleInteractionPost}from"#public/channels/slack/interactions.js";import{verifySlackRequest as verifySlackRequest$1}from"#public/channels/slack/verify.js";const log=createLogger(`slack.channel`);function rebuildSlackContext(e,t,n,r){let{thread:i,slack:a}=buildSlackBinding({api:n,botToken:r?.botToken,channelId:e.channelId??``,threadTs:e.threadTs??``,installationTeamId:e.installationTeamId??void 0,teamId:e.teamId??void 0,onThreadTsChanged(n){e.threadTs=n,e.channelId&&t.continuation?.alias(slackContinuationToken(e.channelId,n))}});return{thread:i,slack:a,state:e}}function slackChannel(e={}){let t=resolveSlackTransportOptions(e.api),n=mergeUploadPolicy(e.uploadPolicy),r=createSlackFetchFile({api:t,botToken:e.credentials?.botToken}),i=e.onInputResponse??defaultOnInputResponse,a=composeSlackRenderers(e.renderers??[],{events:{...defaultEvents,"input.requested":defaultInputRequestedHandler(e.approvalChannel)},received:defaultReceived,taskCard:renderDefaultSlackTaskCard}),o=a.events[`turn.started`],s=withTaskCards({...a.events,"message.completed":withFinalReplyDelivery(a.events[`message.completed`]),async"turn.started"(e,t,n){let r=slackUserIdFromAuthContext(n.session.auth.current);r!==void 0&&(t.state.triggeringUserId=r),await o?.(e,t,n)}},a.taskCard),c=new Set,l=defineChannel({kindHint:`slack`,turnPolicy:e.turnPolicy,state:{channelId:null,threadTs:null,teamId:null,installationTeamId:null,triggeringUserId:null,triggeringMessageTs:null,pendingToolCallMessage:null,lastReasoningTypingAtMs:null,lastReasoningTypingStatus:null,pendingAuthMessageTs:{},pendingApprovalCards:{},slackUsersByPrincipal:{}},fetchFile:r,metadata(e){return{channelId:e.channelId,teamId:e.teamId,threadTs:e.threadTs,triggeringUserId:e.triggeringUserId??null}},audience:({state:e})=>e.audience??`unknown`,context(n,r){return rebuildSlackContext(n,r,t,e.credentials)},deliver(e,t){return recordSlackPrincipal(t.state,t.session.auth.current,e),defaultDeliverResult(withUndeliveredReplyNote(t.state,e))},routes:[POST(e.route??SLACK_CHANNEL_DEFAULT_ROUTE,async(r,o)=>{let{waitUntil:s}=o,{from:l,resolveSession:u}=o,d=await verifyInbound(r,e.credentials);return d===null?new Response(`unauthorized`,{status:401}):shouldDropSlackHttpTimeoutRetry(r.headers)?new Response(`ok`):(r.headers.get(`content-type`)??``).includes(`application/x-www-form-urlencoded`)?handleInteractionPost(d,{from:l,resolveSession:u,waitUntil:s},{api:t,config:e,onInputResponse:i}):handleEventPost({api:t,body:d,from:l,resolveSession:u,waitUntil:s,config:e,received:a.received,uploadPolicy:n,handledEvents:c,headers:r.headers})})],receive(n,{from:r}){return receiveOnSlack(n,{from:r,api:t,credentials:e.credentials})},events:s}),u=e.credentials,d=defineSlackAppManifest({botName:e.botName});return setChannelBuildMetadata(l,e=>({externalCredentials:u?.vercelConnect,manifest:d.build(e)})),l}function defaultOnInputResponse(e){return{auth:e.defaultAuth}}async function receiveOnSlack(e,t){let n=e.target,r=n.channelId;if(!r||typeof r!=`string`)throw Error(`slackChannel().receive requires target.channelId.`);let i=typeof n.threadTs==`string`?n.threadTs:``,a=typeof n.installationTeamId==`string`?n.installationTeamId:t.installationTeamId,o=n.initialMessage;if(o&&i.length>0)throw Error("slackChannel().receive: `threadTs` and `initialMessage` are mutually exclusive.");let s=i;if(o){let{thread:e}=buildSlackBinding({api:t.api,botToken:t.credentials?.botToken,channelId:r,threadTs:``,installationTeamId:a,teamId:t.teamId}),n={card:o.card};o.fallbackText!==void 0&&(n.fallbackText=o.fallbackText),s=(await e.post(n)).id}let c=s||crypto.randomUUID(),l=n.audience===void 0?void 0:normalizeChannelAudience(n.audience),u={channelId:r,installationTeamId:a??null,threadTs:s||null,teamId:t.teamId??null,triggeringUserId:t.triggeringUserId??null};return l!==void 0&&(u.audience=l),t.from(slackContinuationToken(r,c)).send(e.message,{auth:e.auth,state:u,title:e.title})}function shouldDropSlackHttpTimeoutRetry(e){return Number(e.get(`x-slack-retry-num`)??`0`)>=1&&e.get(`x-slack-retry-reason`)===`http_timeout`}async function handleEventPost(e){let{config:t}=e,n,r;try{n=parseSlackWebhookBody(e.body,{headers:e.headers}),r=parseSlackEventEnvelope(e.body)}catch(e){return log.warn(`inbound webhook body is not valid JSON`,{error:e}),new Response(`ok`)}if(n.kind===`url_verification`)return new Response(n.challenge,{status:200,headers:{"content-type":`text/plain`}});if(r===null)return new Response(`ok`);let i=typeof r.api_app_id==`string`?r.api_app_id:void 0,a=slackEventBotUserId(r),o=slackEventReceivingBotUserId(r),s=slackEventInstallationTeamId(r),dispatch=null,c=null,dispatchMessage=(n,r,c)=>()=>dispatchSlackMessage({api:e.api,appId:i,botUserId:a,receivingBotUserId:o,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,handler:c,installationTeamId:s,kind:n,message:r,received:e.received,threadContext:t.threadContext,uploadPolicy:e.uploadPolicy});if(n.kind===`app_mention`||n.kind===`direct_message`){let e=n.kind,r=slackMessageFromWebhookPayload(n,s);if(r!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},r)){let n=(e===`app_mention`?t.onAppMention:t.onDirectMessage)??t.onMessage;n===void 0?c=dispatchMessage(e,r,defaultOnMessage):dispatch=dispatchMessage(e,r,n)}}let l=t.onMessage;if(dispatch===null&&l!==void 0){let e=parseMessageEvent(r);e!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},e)&&(a===void 0||!e.text.includes(`<@${a}`))&&(dispatch=dispatchMessage(`channel_message`,e,l))}let u=t.onEvent;if(dispatch===null&&u!==void 0&&(dispatch=()=>dispatchSlackEvent({api:e.api,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,envelope:r,handler:u,installationTeamId:s})),dispatch??=c,dispatch===null)return new Response(`ok`);let d=r.event_id;if(d){if(e.handledEvents.has(d))return log.warn(`received a duplicate event`,{event_id:d,event_time:r.event_time,retry_num:n.retry?.num??`(null)`,retry_reason:n.retry?.reason??`(null)`}),new Response(`ok`);markEventHandled(d,e.handledEvents)}return e.waitUntil(dispatch()),new Response(`ok`)}function isSelfAuthoredSlackMessage(e,t){return e.botUserId!==void 0&&t.author?.userId===e.botUserId||e.appId!==void 0&&t.raw.app_id===e.appId}async function dispatchSlackMessage(e){let t=slackContinuationToken(e.message.channelId,e.message.threadTs),{thread:n,slack:r}=buildSlackBinding({api:e.api,appId:e.appId,botToken:e.credentials?.botToken,botUserId:e.botUserId,channelId:e.message.channelId,threadTs:e.message.threadTs,installationTeamId:e.installationTeamId,teamId:e.message.teamId}),i=e.message.author,a={channelId:e.message.channelId,installationTeamId:e.installationTeamId??null,teamId:e.message.teamId??null,threadTs:e.message.threadTs,triggeringMessageTs:e.message.ts,triggeringUserId:i?.userId??null},o=bindSlackSessionOperations({address:t,defaultAuth:i===void 0?null:buildSlackAuthContext({channelId:e.message.channelId,fullName:i.fullName,installationTeamId:e.installationTeamId,isBot:i.isBot,teamId:e.message.teamId,threadTs:e.message.threadTs,userId:i.userId,userName:i.userName}),from:e.from,resolveSession:e.resolveSession,state:a}),s,isDMOrPrivateChannel=()=>s??=isPrivateSlackConversation({channelId:e.message.channelId,raw:e.message.raw,request:r.request}),c=e.kind===`app_mention`||e.botUserId!==void 0&&e.message.text.includes(`<@${e.botUserId}`),l={...o,isBotMentioned:()=>c,isDMOrPrivateChannel,isSubscribed:async()=>await o.resolveSession()!==void 0,slack:r,thread:n},u=e.kind===`channel_message`?void 0:acknowledgeMessage(e.received,e.message,{slack:r,thread:n}),d;try{d=await e.handler(l,e.message)}catch(t){logError(log,`${e.kind} handler failed`,t,{channelId:e.message.channelId})}if(d==null){u!==void 0&&(await u,await n.startTyping());return}await(u??acknowledgeMessage(e.received,e.message,{slack:r,thread:n}));let f=await isDMOrPrivateChannel();a.audience=e.kind===`direct_message`||f?`private`:`public`,await deliverSlackMessage({botUserId:e.receivingBotUserId,credentials:e.credentials,kind:e.kind,isPrivateConversation:f,isMentioned:c,message:e.message,result:d,sessionOperations:o,thread:n,threadContext:e.threadContext,uploadPolicy:e.uploadPolicy})}async function acknowledgeMessage(e,t,n){try{await e(t,n)}catch(e){logError(log,`received renderer failed`,e,{channelId:t.channelId})}}async function dispatchSlackEvent(e){let t=e.envelope.event.team_id,n=typeof t==`string`?t:typeof e.envelope.team_id==`string`?e.envelope.team_id:void 0,r=[],sourceFor=t=>e.from(slackContinuationToken(t.channelId,t.threadTs)),i={cancel:({target:e,turnId:t})=>sourceFor(e).cancel({turnId:t}),clear:({target:e})=>sourceFor(e).clear(),compact:({target:e})=>sourceFor(e).compact(),envelope:e.envelope,reset:({reason:e,target:t})=>sourceFor(t).reset({reason:e}),resolveSession:({target:t})=>e.resolveSession(slackContinuationToken(t.channelId,t.threadTs)),respond:(t,{auth:n,target:r})=>sourceFor(r).respond(t,{auth:n,state:withSlackResponder(void 0,typeof e.envelope.event.user==`string`?e.envelope.event.user:void 0)}),send:(t,{auth:r,target:i,title:a})=>receiveOnSlack({auth:r,message:t,target:i,title:a},{from:e.from,api:e.api,credentials:e.credentials,installationTeamId:e.installationTeamId,teamId:n,...typeof e.envelope.event.user==`string`?{triggeringUserId:e.envelope.event.user}:{}}),slack:buildSlackWorkspaceHandle({api:e.api,botToken:e.credentials?.botToken,installationTeamId:e.installationTeamId,teamId:n}),waitUntil(e){r.push(e)}};try{await e.handler(i,e.envelope.event)}catch(t){logError(log,`event handler failed`,t,{eventId:e.envelope.event_id,eventType:e.envelope.event.type})}await Promise.allSettled(r)}async function verifyInbound(e,t){try{return await verifySlackRequest$1(e,{signingSecret:t?.signingSecret??(t?.webhookVerifier?void 0:process.env.SLACK_SIGNING_SECRET),webhookVerifier:t?.webhookVerifier})}catch(e){return log.warn(`slack inbound verification failed`,{error:e}),null}}async function deliverSlackMessage(e){let{message:t,thread:n}=e;try{let r=e.threadContext===void 0?[]:await loadThreadContextMessages(n,t,e.threadContext),i=formatSlackThreadContext(r),a=await collectInboundFileParts({mention:t,thread:n,policy:e.uploadPolicy}),o={botUserId:e.botUserId,channelId:t.channelId,fullName:t.author?.fullName,isMentioned:e.isMentioned,teamId:t.teamId,threadTs:t.threadTs,userId:t.author?.userId??``,userName:t.author?.userName},s=formatSlackInboundMessage(o,t),c=buildSlackTurnMessage(i===void 0?s:`${i}\n\n${s}`,a),l=e.result.context??[],u=e.isPrivateConversation?`Private message`:e.result.title??t.markdown,d=l.length===0?{auth:e.result.auth,title:u}:{auth:e.result.auth,context:l,title:u};await e.sessionOperations.send(c,d)}catch(n){logError(log,`${e.kind} delivery failed`,n,{channelId:t.channelId})}}function recordSlackPrincipal(e,t,n){if(t===null)return;let r=n.state?.triggeringUserId,i=slackUserIdFromAuthContext(t)??(typeof r==`string`?r:void 0);i!==void 0&&(e.slackUsersByPrincipal={...e.slackUsersByPrincipal,[t.principalId]:i})}export{slackChannel};
|
|
1
|
+
import{markEventHandled}from"./utils.js";import{attachInputText}from"#internal/input-text.js";import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{setChannelBuildMetadata}from"#channel/compiled-channel.js";import{defaultDeliverResult}from"#channel/adapter.js";import{POST,defineChannel}from"#public/definitions/channel.js";import{mergeUploadPolicy}from"#public/channels/upload-policy.js";import{resolveSlackTransportOptions}from"#public/channels/slack/transport.js";import{buildSlackAuthContext,slackUserIdFromAuthContext}from"#public/channels/slack/auth.js";import{buildSlackBinding,buildSlackWorkspaceHandle,slackContinuationToken}from"#public/channels/slack/api.js";import{defaultInputRequestedHandler}from"#public/channels/slack/approval-cards.js";import{withFinalReplyDelivery,withUndeliveredReplyNote}from"#public/channels/slack/reply-delivery.js";import{defaultEvents,defaultOnMessage,defaultReceived}from"#public/channels/slack/defaults.js";import{composeSlackRenderers}from"#public/channels/slack/renderers.js";import{loadThreadContextMessages}from"#public/channels/slack/thread.js";import{parseSlackWebhookBody}from"#compiled/@chat-adapter/slack/webhook.js";import{bindSlackSessionOperations,withSlackResponder}from"#public/channels/slack/session-operations.js";import{buildSlackTurnMessage,collectInboundFileParts,createSlackFetchFile}from"#public/channels/slack/attachments.js";import{renderDefaultSlackTaskCard,withTaskCards}from"#public/channels/slack/task-card.js";import{parseMessageEvent,parseSlackEventEnvelope,slackEventBotUserId,slackEventInstallationTeamId,slackEventReceivingBotUserId,slackMessageFromWebhookPayload}from"#public/channels/slack/inbound.js";import{formatSlackInboundMessage,formatSlackThreadContext}from"#public/channels/slack/model-context.js";import{isPrivateSlackConversation}from"#public/channels/slack/privacy.js";import{SLACK_CHANNEL_DEFAULT_ROUTE}from"#public/channels/slack/constants.js";import{defineSlackAppManifest}from"#public/channels/slack/app-manifest.js";import{handleInteractionPost}from"#public/channels/slack/interactions.js";import{verifySlackRequest as verifySlackRequest$1}from"#public/channels/slack/verify.js";const log=createLogger(`slack.channel`);function rebuildSlackContext(e,t,n,r){let{thread:i,slack:a}=buildSlackBinding({api:n,botToken:r?.botToken,channelId:e.channelId??``,threadTs:e.threadTs??``,installationTeamId:e.installationTeamId??void 0,teamId:e.teamId??void 0,onThreadTsChanged(n){e.threadTs=n,e.channelId&&t.continuation?.alias(slackContinuationToken(e.channelId,n))}});return{thread:i,slack:a,state:e}}function slackChannel(e={}){let t=resolveSlackTransportOptions(e.api),n=mergeUploadPolicy(e.uploadPolicy),r=createSlackFetchFile({api:t,botToken:e.credentials?.botToken}),i=e.onInputResponse??defaultOnInputResponse,a=composeSlackRenderers(e.renderers??[],{events:{...defaultEvents,"input.requested":defaultInputRequestedHandler(e.approvalChannel)},received:defaultReceived,taskCard:renderDefaultSlackTaskCard}),o=a.events[`turn.started`],s=withTaskCards({...a.events,"message.completed":withFinalReplyDelivery(a.events[`message.completed`]),async"turn.started"(e,t,n){let r=slackUserIdFromAuthContext(n.session.auth.current);r!==void 0&&(t.state.triggeringUserId=r),await o?.(e,t,n)}},a.taskCard),c=new Set,l=defineChannel({kindHint:`slack`,turnPolicy:e.turnPolicy,state:{channelId:null,threadTs:null,teamId:null,installationTeamId:null,triggeringUserId:null,triggeringMessageTs:null,pendingToolCallMessage:null,pendingAuthMessageTs:{},pendingApprovalCards:{},slackUsersByPrincipal:{}},fetchFile:r,metadata(e){return{channelId:e.channelId,teamId:e.teamId,threadTs:e.threadTs,triggeringUserId:e.triggeringUserId??null}},audience:({state:e})=>e.audience??`unknown`,context(n,r){return rebuildSlackContext(n,r,t,e.credentials)},deliver(e,t){return recordSlackPrincipal(t.state,t.session.auth.current,e),defaultDeliverResult(withUndeliveredReplyNote(t.state,e))},routes:[POST(e.route??SLACK_CHANNEL_DEFAULT_ROUTE,async(r,o)=>{let{waitUntil:s}=o,{from:l,resolveSession:u}=o,d=await verifyInbound(r,e.credentials);return d===null?new Response(`unauthorized`,{status:401}):shouldDropSlackHttpTimeoutRetry(r.headers)?new Response(`ok`):(r.headers.get(`content-type`)??``).includes(`application/x-www-form-urlencoded`)?handleInteractionPost(d,{from:l,resolveSession:u,waitUntil:s},{api:t,config:e,onInputResponse:i}):handleEventPost({api:t,body:d,from:l,resolveSession:u,waitUntil:s,config:e,received:a.received,uploadPolicy:n,handledEvents:c,headers:r.headers})})],receive(n,{from:r}){return receiveOnSlack(n,{from:r,api:t,credentials:e.credentials})},events:s}),u=e.credentials,d=defineSlackAppManifest({botName:e.botName});return setChannelBuildMetadata(l,e=>({externalCredentials:u?.vercelConnect,manifest:d.build(e)})),l}function defaultOnInputResponse(e){return{auth:e.defaultAuth}}async function receiveOnSlack(e,t){let n=e.target,r=n.channelId;if(!r||typeof r!=`string`)throw Error(`slackChannel().receive requires target.channelId.`);let i=typeof n.threadTs==`string`?n.threadTs:``,a=typeof n.installationTeamId==`string`?n.installationTeamId:t.installationTeamId,o=n.initialMessage;if(o&&i.length>0)throw Error("slackChannel().receive: `threadTs` and `initialMessage` are mutually exclusive.");let s=i;if(o){let{thread:e}=buildSlackBinding({api:t.api,botToken:t.credentials?.botToken,channelId:r,threadTs:``,installationTeamId:a,teamId:t.teamId}),n={card:o.card};o.fallbackText!==void 0&&(n.fallbackText=o.fallbackText),s=(await e.post(n)).id}let c=s||crypto.randomUUID(),l=n.audience===void 0?void 0:normalizeChannelAudience(n.audience),u={channelId:r,installationTeamId:a??null,threadTs:s||null,teamId:t.teamId??null,triggeringUserId:t.triggeringUserId??null};return l!==void 0&&(u.audience=l),t.from(slackContinuationToken(r,c)).send(e.message,{auth:e.auth,state:u,title:e.title})}function shouldDropSlackHttpTimeoutRetry(e){return Number(e.get(`x-slack-retry-num`)??`0`)>=1&&e.get(`x-slack-retry-reason`)===`http_timeout`}async function handleEventPost(e){let{config:t}=e,n,r;try{n=parseSlackWebhookBody(e.body,{headers:e.headers}),r=parseSlackEventEnvelope(e.body)}catch(e){return log.warn(`inbound webhook body is not valid JSON`,{error:e}),new Response(`ok`)}if(n.kind===`url_verification`)return new Response(n.challenge,{status:200,headers:{"content-type":`text/plain`}});if(r===null)return new Response(`ok`);let i=typeof r.api_app_id==`string`?r.api_app_id:void 0,a=slackEventBotUserId(r),o=slackEventReceivingBotUserId(r),s=slackEventInstallationTeamId(r),dispatch=null,c=null,dispatchMessage=(n,r,c)=>()=>dispatchSlackMessage({api:e.api,appId:i,botUserId:a,receivingBotUserId:o,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,handler:c,installationTeamId:s,kind:n,message:r,received:e.received,resolvesTypedInput:t.onInputResponse===void 0&&t.approvalChannel===void 0,threadContext:t.threadContext,uploadPolicy:e.uploadPolicy});if(n.kind===`app_mention`||n.kind===`direct_message`){let e=n.kind,r=slackMessageFromWebhookPayload(n,s);if(r!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},r)){let n=(e===`app_mention`?t.onAppMention:t.onDirectMessage)??t.onMessage;n===void 0?c=dispatchMessage(e,r,defaultOnMessage):dispatch=dispatchMessage(e,r,n)}}let l=t.onMessage;if(dispatch===null&&l!==void 0){let e=parseMessageEvent(r);e!==null&&!isSelfAuthoredSlackMessage({appId:i,botUserId:a},e)&&(a===void 0||!e.text.includes(`<@${a}`))&&(dispatch=dispatchMessage(`channel_message`,e,l))}let u=t.onEvent;if(dispatch===null&&u!==void 0&&(dispatch=()=>dispatchSlackEvent({api:e.api,from:e.from,resolveSession:e.resolveSession,credentials:t.credentials,envelope:r,handler:u,installationTeamId:s})),dispatch??=c,dispatch===null)return new Response(`ok`);let d=r.event_id;if(d){if(e.handledEvents.has(d))return log.warn(`received a duplicate event`,{event_id:d,event_time:r.event_time,retry_num:n.retry?.num??`(null)`,retry_reason:n.retry?.reason??`(null)`}),new Response(`ok`);markEventHandled(d,e.handledEvents)}return e.waitUntil(dispatch()),new Response(`ok`)}function isSelfAuthoredSlackMessage(e,t){return e.botUserId!==void 0&&t.author?.userId===e.botUserId||e.appId!==void 0&&t.raw.app_id===e.appId}async function dispatchSlackMessage(e){let t=slackContinuationToken(e.message.channelId,e.message.threadTs),{thread:n,slack:r}=buildSlackBinding({api:e.api,appId:e.appId,botToken:e.credentials?.botToken,botUserId:e.botUserId,channelId:e.message.channelId,threadTs:e.message.threadTs,installationTeamId:e.installationTeamId,teamId:e.message.teamId}),i=e.message.author,a={channelId:e.message.channelId,installationTeamId:e.installationTeamId??null,teamId:e.message.teamId??null,threadTs:e.message.threadTs,triggeringMessageTs:e.message.ts,triggeringUserId:i?.userId??null},o=bindSlackSessionOperations({address:t,defaultAuth:i===void 0?null:buildSlackAuthContext({channelId:e.message.channelId,fullName:i.fullName,installationTeamId:e.installationTeamId,isBot:i.isBot,teamId:e.message.teamId,threadTs:e.message.threadTs,userId:i.userId,userName:i.userName}),from:e.from,resolveSession:e.resolveSession,state:a}),s,isDMOrPrivateChannel=()=>s??=isPrivateSlackConversation({channelId:e.message.channelId,raw:e.message.raw,request:r.request}),c=e.kind===`app_mention`||e.botUserId!==void 0&&e.message.text.includes(`<@${e.botUserId}`),l={...o,isBotMentioned:()=>c,isDMOrPrivateChannel,isSubscribed:async()=>await o.resolveSession()!==void 0,slack:r,thread:n},u=e.kind===`channel_message`?void 0:acknowledgeMessage(e.received,e.message,{slack:r,thread:n}),d;try{d=await e.handler(l,e.message)}catch(t){logError(log,`${e.kind} handler failed`,t,{channelId:e.message.channelId})}if(d==null){u!==void 0&&(await u,await n.startTyping());return}await(u??acknowledgeMessage(e.received,e.message,{slack:r,thread:n}));let f=await isDMOrPrivateChannel();a.audience=e.kind===`direct_message`||f?`private`:`public`,await deliverSlackMessage({botUserId:e.receivingBotUserId,credentials:e.credentials,kind:e.kind,isPrivateConversation:f,isMentioned:c,message:e.message,resolvesTypedInput:e.resolvesTypedInput,result:d,sessionOperations:o,thread:n,threadContext:e.threadContext,uploadPolicy:e.uploadPolicy})}async function acknowledgeMessage(e,t,n){try{await e(t,n)}catch(e){logError(log,`received renderer failed`,e,{channelId:t.channelId})}}async function dispatchSlackEvent(e){let t=e.envelope.event.team_id,n=typeof t==`string`?t:typeof e.envelope.team_id==`string`?e.envelope.team_id:void 0,r=[],sourceFor=t=>e.from(slackContinuationToken(t.channelId,t.threadTs)),i={cancel:({target:e,turnId:t})=>sourceFor(e).cancel({turnId:t}),clear:({target:e})=>sourceFor(e).clear(),compact:({target:e})=>sourceFor(e).compact(),envelope:e.envelope,reset:({reason:e,target:t})=>sourceFor(t).reset({reason:e}),resolveSession:({target:t})=>e.resolveSession(slackContinuationToken(t.channelId,t.threadTs)),respond:(t,{auth:n,target:r})=>sourceFor(r).respond(t,{auth:n,state:withSlackResponder(void 0,typeof e.envelope.event.user==`string`?e.envelope.event.user:void 0)}),send:(t,{auth:r,target:i,title:a})=>receiveOnSlack({auth:r,message:t,target:i,title:a},{from:e.from,api:e.api,credentials:e.credentials,installationTeamId:e.installationTeamId,teamId:n,...typeof e.envelope.event.user==`string`?{triggeringUserId:e.envelope.event.user}:{}}),slack:buildSlackWorkspaceHandle({api:e.api,botToken:e.credentials?.botToken,installationTeamId:e.installationTeamId,teamId:n}),waitUntil(e){r.push(e)}};try{await e.handler(i,e.envelope.event)}catch(t){logError(log,`event handler failed`,t,{eventId:e.envelope.event_id,eventType:e.envelope.event.type})}await Promise.allSettled(r)}async function verifyInbound(e,t){try{return await verifySlackRequest$1(e,{signingSecret:t?.signingSecret??(t?.webhookVerifier?void 0:process.env.SLACK_SIGNING_SECRET),webhookVerifier:t?.webhookVerifier})}catch(e){return log.warn(`slack inbound verification failed`,{error:e}),null}}async function deliverSlackMessage(e){let{message:t,thread:n}=e;try{let r=e.threadContext===void 0?[]:await loadThreadContextMessages(n,t,e.threadContext),i=formatSlackThreadContext(r),a=await collectInboundFileParts({mention:t,thread:n,policy:e.uploadPolicy}),o={botUserId:e.botUserId,channelId:t.channelId,fullName:t.author?.fullName,isMentioned:e.isMentioned,teamId:t.teamId,threadTs:t.threadTs,userId:t.author?.userId??``,userName:t.author?.userName},s=formatSlackInboundMessage(o,t),c=buildSlackTurnMessage(i===void 0?s:`${i}\n\n${s}`,a),l=e.result.context??[],u=e.isPrivateConversation?`Private message`:e.result.title??t.markdown,d=attachInputText(l.length===0?{auth:e.result.auth,title:u}:{auth:e.result.auth,context:l,title:u},e.resolvesTypedInput&&a.length===0?slackTypedText(t.text,e.botUserId):void 0);await e.sessionOperations.send(c,d)}catch(n){logError(log,`${e.kind} delivery failed`,n,{channelId:t.channelId})}}function slackTypedText(e,t){return t===void 0?e:e.replace(RegExp(`<@${t}(?:\\|[^>]*)?>`,`gu`),``).trim()}function recordSlackPrincipal(e,t,n){if(t===null)return;let r=n.state?.triggeringUserId,i=slackUserIdFromAuthContext(t)??(typeof r==`string`?r:void 0);i!==void 0&&(e.slackUsersByPrincipal={...e.slackUsersByPrincipal,[t.principalId]:i})}export{slackChannel};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{contextStorage}from"#context/container.js";import{ScheduleIdKey}from"#context/keys.js";import{displayName}from"#shared/display-name.js";import{AGENT_TOOL_NAME}from"#tools/framework/agent-contract.js";import{formatDuration}from"#shared/format-duration.js";import{truncateMessageText}from"#public/channels/slack/limits.js";import{closeSettledTasks,taskCardView,trackTaskCardEvent}from"#channel/task-card.js";const log=createLogger(`slack.task-card`);function renderDefaultSlackTaskCard(e){if(e.tasks.length===0)return null;let t=collapseEarlierRows(e.tasks).map(toSlackTask),n=planTitle(e),r=slackBlockId(e.turnId),i=t.length===1?[{block_id:r,type:`task_card`,...t[0]}]:[{block_id:r,type:`plan`,tasks:t,title:n}],a=e.tasks.map(e=>e.title).join(`, `);return{blocks:i,text:truncateMessageText(`${n}: ${a}`)}}function planTitle(e){let{tasks:t}=e;if(e.state===`blocked`){let e=t.find(e=>e.blockedOn!==void 0)?.blockedOn;return waitingText(e?.kind??`input`)}let n=t.length,names=e=>n>1?agentNames(e):void 0;if(e.state===`working`){let e=t.filter(e=>e.status===`working`),r=n-e.length;if(r===0){let e=names(t);return e===void 0?`Working on ${countTasks(n)}`:`Asking ${e}`}let i=`${String(r)} of ${countTasks(n)} done`,a=names(e);return a===void 0?i:`Waiting on ${a} · ${i}`}let r=t.filter(e=>e.status===`failed`).length,i=t.filter(isInterrupted).length,a=[];r>0&&a.push(`${String(r)} failed`),i>0&&a.push(`${String(i)} stopped`);let o=turnDuration(t),s=`Finished ${countTasks(n)}${o===void 0?``:` in ${o}`}`;return a.length===0?s:`${s}: ${a.join(`, `)}`}function agentNames(e){if(e.some(e=>e.kind!==`agent`||e.name===AGENT_TOOL_NAME))return;let t=[...new Set(e.map(e=>displayName(e.name)))];if(!(t.length===0||t.length>3))return t.length<=2?t.join(` and `):`${t.slice(0,-1).join(`, `)}, and ${t.at(-1)}`}function turnDuration(e){let t=e.flatMap(e=>e.settledAt===void 0?[]:[Date.parse(e.settledAt)]);if(t.length===0)return;let n=Math.min(...e.map(e=>Date.parse(e.startedAt)));return shownDuration(Math.max(...t)-n)}function taskDuration(e){if(e.settledAt!==void 0)return shownDuration(Date.parse(e.settledAt)-Date.parse(e.startedAt))}function shownDuration(e){return Number.isFinite(e)&&e>=1e3?formatDuration(e):void 0}function isInterrupted(e){return e.status===`cancelled`&&e.cancelReason!==`task_cancel`}function countTasks(e){return e===1?`1 task`:`${String(e)} tasks`}function collapseEarlierRows(e){if(e.length<=50)return e;let t=e.length-49,n=new Set(e.filter(e=>e.status!==`working`).slice(0,t).map(e=>e.id)),r=e.filter(e=>!n.has(e.id));if(n.size===0)return r.slice(-50);let i=e.some(e=>n.has(e.id)&&(e.status===`failed`||isInterrupted(e)));return[{id:`earlier`,kind:`tool`,name:`earlier`,startedAt:e[0].startedAt,status:i?`failed`:`completed`,taskId:`earlier`,title:`${countTasks(n.size)} finished earlier`},...r].slice(-50)}function toSlackTask(e){let t={status:slackStatus(e),task_id:slackTaskId(e.id),title:truncate(e.title,80)};e.blockedOn!==void 0&&(t.details=richText(blockerLine(e.blockedOn)));let n=outputLine(e);return n!==void 0&&(t.output=richText(n)),t}function slackStatus(e){switch(e.status){case`working`:case`blocked`:return`in_progress`;case`completed`:return`complete`;case`cancelled`:return isInterrupted(e)?`error`:`complete`;case`failed`:return`error`}}function outputLine(e){let t=taskDuration(e);switch(e.status){case`completed`:{if(t===void 0)return e.summary;let n=`Done in ${t}`;return e.summary===void 0?n:`${n}: ${e.summary}`}case`failed`:{let n=t===void 0?`Failed`:`Failed after ${t}`;return e.summary===void 0?n:`${n}: ${e.summary}`}case`cancelled`:return stoppedLine(e.cancelReason);default:return}}function stoppedLine(e){switch(e){case`task_cancel`:return`Stopped early since it was no longer needed`;case`turn_cancelled`:return`Stopped by request`;case`turn_ended`:return`Stopped when the turn ended`;case void 0:return`Stopped`}}function blockerLine(e){let t=waitingText(e.kind);return e.label===void 0?t:`${t}: ${e.label}`}function waitingText(e){switch(e){case`approval`:return`Waiting for approval`;case`authorization`:return`Waiting for sign-in`;case`input`:return`Waiting for a response`}}function richText(e){return{elements:[{elements:[{text:truncate(e,100),type:`text`}],type:`rich_text_section`}],type:`rich_text`}}function truncate(e,t){if(e.length<=t)return e;let n=e.slice(0,t-1),r=n.lastIndexOf(` `);return`${(r>t/2?n.slice(0,r):n).trimEnd()}…`}function slackTaskId(e){return e.replace(/[^a-zA-Z0-9_-]/g,`_`).slice(-200)}function slackBlockId(e){return`eve_task_card_${slackTaskId(e)}`}const TRACKED_EVENTS=[`actions.requested`,`action.result`,`task.started`,`task.settled`,`input.requested`,`input.resolved`,`authorization.required`,`authorization.completed`,`turn.completed`,`turn.failed`,`turn.cancelled`];function withTaskCards(e,t){let n={};for(let r of TRACKED_EVENTS){let i=e[r];n[r]=async(e,n,a)=>{let o={data:e,type:r};o.type===`task.started`&&await closeSettledCard(n,o.data,t);let s=trackTaskCardEvent(trackedTurns(n.state),o,new Date().toISOString());for(let[e,t]of Object.entries(s))rememberTurn(n.state,e,t);try{await i?.(e,n,a)}finally{for(let e of Object.keys(s))await writeTaskCard(n,e,t)}}}return{...e,...n}}async function closeSettledCard(e,t,n){let r=e.state.taskCards?.[t.turnId],i=r&&closeSettledTasks(r.turn,t.callId);i!==void 0&&(e.state.taskCards={...e.state.taskCards,[t.turnId]:{...r,turn:i.closed}},await writeTaskCard(e,t.turnId,n),e.state.taskCards={...e.state.taskCards,[t.turnId]:{turn:i.open}})}function trackedTurns(e){return Object.fromEntries(Object.entries(e.taskCards??{}).map(([e,t])=>[e,t.turn]))}function rememberTurn(e,t,n){let r={...e.taskCards,[t]:{...e.taskCards?.[t],turn:n}},i=Object.keys(r);for(let e of i.slice(0,Math.max(0,i.length-20)))delete r[e];e.taskCards=r}async function writeTaskCard(e,t,n){let r=e.state.taskCards?.[t];if(r===void 0)return;let i=normalizeChannelAudience(e.state.audience),a=taskCardView(t,r.turn,{audience:i}),{channelId:o,threadTs:s}=e.state,c=contextStorage.getStore()?.get(ScheduleIdKey)!==void 0,l=o&&s&&!c?{channelId:o,threadTs:s}:void 0,u=l===void 0?null:renderCard(n,a);if(l!==void 0&&u!==null){let n=createHash(`sha256`).update(JSON.stringify(u)).digest(`base64url`);if(n!==r.fingerprint){let i=await writeCardWithRetry(e.slack,{...l,card:u,ts:r.ts});if(i===void 0)return;e.state.taskCards={...e.state.taskCards,[t]:{fingerprint:n,ts:i,turn:r.turn}}}}a.state===`finished`&&forgetTurn(e.state,t)}function forgetTurn(e,t){let{[t]:n,...r}=e.taskCards??{};e.taskCards=r}function renderCard(e,t){try{return e(t)}catch(e){return logError(log,`task card renderer failed`,e,{turnId:t.turnId}),null}}async function writeCardWithRetry(e,t){for(let n of[1,2])try{return await writeCard(e,t)}catch(e){logError(log,`task card write failed`,e,{attempt:n}),n===1&&await new Promise(e=>setTimeout(e,1e3))}}async function writeCard(e,t){let n={blocks:t.card.blocks,channel:t.channelId,text:t.card.text};if(t.ts!==void 0){let r=await e.request(`chat.update`,{...n,ts:t.ts});if(r.ok===!0)return t.ts;if(r.error!==`message_not_found`)throw slackError(r.error)}let r=await e.request(`chat.postMessage`,{...n,thread_ts:t.threadTs,unfurl_links:!1,unfurl_media:!1});if(r.ok!==!0)throw slackError(r.error);if(typeof r.ts!=`string`||r.ts===``)throw Error(`Slack did not return a ts for the task card.`);return r.ts}function slackError(e){return Error(`Slack task card failed: ${e??`unknown_error`}`)}export{renderDefaultSlackTaskCard,withTaskCards};
|
|
1
|
+
import{createHash}from"node:crypto";import{createLogger,logError}from"#internal/logging.js";import{normalizeChannelAudience}from"#shared/channel-audience.js";import{contextStorage}from"#context/container.js";import{ScheduleIdKey}from"#context/keys.js";import{displayName}from"#shared/display-name.js";import{AGENT_TOOL_NAME}from"#tools/framework/agent-contract.js";import{formatDuration}from"#shared/format-duration.js";import{truncateMessageText}from"#public/channels/slack/limits.js";import{closeSettledTasks,taskCardView,trackTaskCardEvent}from"#channel/task-card.js";import{restoreStatus}from"#public/channels/slack/thread-status.js";const log=createLogger(`slack.task-card`);function renderDefaultSlackTaskCard(e){if(e.tasks.length===0)return null;let t=collapseEarlierRows(e.tasks).map(toSlackTask),n=planTitle(e),r=slackBlockId(e.turnId),i=t.length===1?[{block_id:r,type:`task_card`,...t[0]}]:[{block_id:r,type:`plan`,tasks:t,title:n}],a=e.tasks.map(e=>e.title).join(`, `);return{blocks:i,text:truncateMessageText(`${n}: ${a}`)}}function planTitle(e){let{tasks:t}=e;if(e.state===`blocked`){let e=t.find(e=>e.blockedOn!==void 0)?.blockedOn;return waitingText(e?.kind??`input`)}let n=t.length,names=e=>n>1?agentNames(e):void 0;if(e.state===`working`){let e=t.filter(e=>e.status===`working`),r=n-e.length;if(r===0){let e=names(t);return e===void 0?`Working on ${countTasks(n)}`:`Asking ${e}`}let i=`${String(r)} of ${countTasks(n)} done`,a=names(e);return a===void 0?i:`Waiting on ${a} · ${i}`}let r=t.filter(e=>e.status===`failed`).length,i=t.filter(isInterrupted).length,a=[];r>0&&a.push(`${String(r)} failed`),i>0&&a.push(`${String(i)} stopped`);let o=turnDuration(t),s=`Finished ${countTasks(n)}${o===void 0?``:` in ${o}`}`;return a.length===0?s:`${s}: ${a.join(`, `)}`}function agentNames(e){if(e.some(e=>e.kind!==`agent`||e.name===AGENT_TOOL_NAME))return;let t=[...new Set(e.map(e=>displayName(e.name)))];if(!(t.length===0||t.length>3))return t.length<=2?t.join(` and `):`${t.slice(0,-1).join(`, `)}, and ${t.at(-1)}`}function turnDuration(e){let t=e.flatMap(e=>e.settledAt===void 0?[]:[Date.parse(e.settledAt)]);if(t.length===0)return;let n=Math.min(...e.map(e=>Date.parse(e.startedAt)));return shownDuration(Math.max(...t)-n)}function taskDuration(e){if(e.settledAt!==void 0)return shownDuration(Date.parse(e.settledAt)-Date.parse(e.startedAt))}function shownDuration(e){return Number.isFinite(e)&&e>=1e3?formatDuration(e):void 0}function isInterrupted(e){return e.status===`cancelled`&&e.cancelReason!==`task_cancel`}function countTasks(e){return e===1?`1 task`:`${String(e)} tasks`}function collapseEarlierRows(e){if(e.length<=50)return e;let t=e.length-49,n=new Set(e.filter(e=>e.status!==`working`).slice(0,t).map(e=>e.id)),r=e.filter(e=>!n.has(e.id));if(n.size===0)return r.slice(-50);let i=e.some(e=>n.has(e.id)&&(e.status===`failed`||isInterrupted(e)));return[{id:`earlier`,kind:`tool`,name:`earlier`,startedAt:e[0].startedAt,status:i?`failed`:`completed`,taskId:`earlier`,title:`${countTasks(n.size)} finished earlier`},...r].slice(-50)}function toSlackTask(e){let t={status:slackStatus(e),task_id:slackTaskId(e.id),title:truncate(e.title,80)};e.blockedOn!==void 0&&(t.details=richText(blockerLine(e.blockedOn)));let n=outputLine(e);return n!==void 0&&(t.output=richText(n)),t}function slackStatus(e){switch(e.status){case`working`:case`blocked`:return`in_progress`;case`completed`:return`complete`;case`cancelled`:return isInterrupted(e)?`error`:`complete`;case`failed`:return`error`}}function outputLine(e){let t=taskDuration(e);switch(e.status){case`completed`:{if(t===void 0)return e.summary;let n=`Done in ${t}`;return e.summary===void 0?n:`${n}: ${e.summary}`}case`failed`:{let n=t===void 0?`Failed`:`Failed after ${t}`;return e.summary===void 0?n:`${n}: ${e.summary}`}case`cancelled`:return stoppedLine(e.cancelReason);default:return}}function stoppedLine(e){switch(e){case`task_cancel`:return`Stopped early since it was no longer needed`;case`turn_cancelled`:return`Stopped by request`;case`turn_ended`:return`Stopped when the turn ended`;case void 0:return`Stopped`}}function blockerLine(e){let t=waitingText(e.kind);return e.label===void 0?t:`${t}: ${e.label}`}function waitingText(e){switch(e){case`approval`:return`Waiting for approval`;case`authorization`:return`Waiting for sign-in`;case`input`:return`Waiting for a response`}}function richText(e){return{elements:[{elements:[{text:truncate(e,100),type:`text`}],type:`rich_text_section`}],type:`rich_text`}}function truncate(e,t){if(e.length<=t)return e;let n=e.slice(0,t-1),r=n.lastIndexOf(` `);return`${(r>t/2?n.slice(0,r):n).trimEnd()}…`}function slackTaskId(e){return e.replace(/[^a-zA-Z0-9_-]/g,`_`).slice(-200)}function slackBlockId(e){return`eve_task_card_${slackTaskId(e)}`}const TRACKED_EVENTS=[`actions.requested`,`action.result`,`task.started`,`task.settled`,`input.requested`,`input.resolved`,`authorization.required`,`authorization.completed`,`turn.completed`,`turn.failed`,`turn.cancelled`];function withTaskCards(e,t){let n={};for(let r of TRACKED_EVENTS){let i=e[r];n[r]=async(e,n,a)=>{let o={data:e,type:r};o.type===`task.started`&&await closeSettledCard(n,o.data,t);let s=trackTaskCardEvent(trackedTurns(n.state),o,new Date().toISOString());for(let[e,t]of Object.entries(s))rememberTurn(n.state,e,t);try{await i?.(e,n,a)}finally{for(let e of Object.keys(s))await writeTaskCard(n,e,t)}}}return{...e,...n}}async function closeSettledCard(e,t,n){let r=e.state.taskCards?.[t.turnId],i=r&&closeSettledTasks(r.turn,t.callId);i!==void 0&&(e.state.taskCards={...e.state.taskCards,[t.turnId]:{...r,turn:i.closed}},await writeTaskCard(e,t.turnId,n),e.state.taskCards={...e.state.taskCards,[t.turnId]:{turn:i.open}})}function trackedTurns(e){return Object.fromEntries(Object.entries(e.taskCards??{}).map(([e,t])=>[e,t.turn]))}function rememberTurn(e,t,n){let r={...e.taskCards,[t]:{...e.taskCards?.[t],turn:n}},i=Object.keys(r);for(let e of i.slice(0,Math.max(0,i.length-20)))delete r[e];e.taskCards=r}async function writeTaskCard(e,t,n){let r=e.state.taskCards?.[t];if(r===void 0)return;let i=normalizeChannelAudience(e.state.audience),a=taskCardView(t,r.turn,{audience:i}),{channelId:o,threadTs:s}=e.state,c=contextStorage.getStore()?.get(ScheduleIdKey)!==void 0,l=o&&s&&!c?{channelId:o,threadTs:s}:void 0,u=l===void 0?null:renderCard(n,a);if(l!==void 0&&u!==null){let n=createHash(`sha256`).update(JSON.stringify(u)).digest(`base64url`);if(n!==r.fingerprint){let i=await writeCardWithRetry(e.slack,{...l,card:u,ts:r.ts});if(i===void 0)return;e.state.taskCards={...e.state.taskCards,[t]:{fingerprint:n,ts:i,turn:r.turn}},i!==r.ts&&a.state===`working`&&await restoreStatus(e)}}a.state===`finished`&&forgetTurn(e.state,t)}function forgetTurn(e,t){let{[t]:n,...r}=e.taskCards??{};e.taskCards=r}function renderCard(e,t){try{return e(t)}catch(e){return logError(log,`task card renderer failed`,e,{turnId:t.turnId}),null}}async function writeCardWithRetry(e,t){for(let n of[1,2])try{return await writeCard(e,t)}catch(e){logError(log,`task card write failed`,e,{attempt:n}),n===1&&await new Promise(e=>setTimeout(e,1e3))}}async function writeCard(e,t){let n={blocks:t.card.blocks,channel:t.channelId,text:t.card.text};if(t.ts!==void 0){let r=await e.request(`chat.update`,{...n,ts:t.ts});if(r.ok===!0)return t.ts;if(r.error!==`message_not_found`)throw slackError(r.error)}let r=await e.request(`chat.postMessage`,{...n,thread_ts:t.threadTs,unfurl_links:!1,unfurl_media:!1});if(r.ok!==!0)throw slackError(r.error);if(typeof r.ts!=`string`||r.ts===``)throw Error(`Slack did not return a ts for the task card.`);return r.ts}function slackError(e){return Error(`Slack task card failed: ${e??`unknown_error`}`)}export{renderDefaultSlackTaskCard,withTaskCards};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SlackEventContext } from "#public/channels/slack/slackChannel.js";
|
|
2
|
+
/** The status eve's default renderer last set in the thread. */
|
|
3
|
+
export interface SlackThreadStatus {
|
|
4
|
+
readonly atMs: number;
|
|
5
|
+
readonly text: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sets the thread status, unless the same text is already showing. `force`
|
|
9
|
+
* writes it anyway, for moments Slack may have cleared it, and so a long turn
|
|
10
|
+
* keeps it from timing out.
|
|
11
|
+
*/
|
|
12
|
+
export declare function showStatus(channel: SlackEventContext, text: string, options?: {
|
|
13
|
+
readonly force?: boolean;
|
|
14
|
+
}): Promise<void>;
|
|
15
|
+
/** Shows the last status again, if any. */
|
|
16
|
+
export declare function restoreStatus(channel: SlackEventContext): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Clears the thread status but keeps its text, so the turn shows it again when
|
|
19
|
+
* it resumes. It is no longer showing, so the next write always goes out.
|
|
20
|
+
*/
|
|
21
|
+
export declare function hideStatus(channel: SlackEventContext): Promise<void>;
|
|
22
|
+
/** Clears the thread status and forgets it: the turn has nothing more to show. */
|
|
23
|
+
export declare function clearStatus(channel: SlackEventContext): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* The newest readable piece of a reasoning block so far: its latest heading
|
|
26
|
+
* when the model titles its reasoning, as reasoning summaries do, otherwise
|
|
27
|
+
* its latest finished sentence. A sentence still streaming counts once it
|
|
28
|
+
* fills the status, so the status never shows half a short sentence. A
|
|
29
|
+
* `complete` block has nothing streaming: its last sentence is finished.
|
|
30
|
+
*/
|
|
31
|
+
export declare function reasoningStatus(reasoning: string, options?: {
|
|
32
|
+
readonly complete?: boolean;
|
|
33
|
+
}): string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{SLACK_TYPING_STATUS_MAX_LENGTH,truncateTypingStatus}from"#public/channels/slack/limits.js";async function showStatus(e,t,n){let r=truncateTypingStatus(t),i=Date.now(),a=e.state.threadStatus;a?.text===r&&i-a.atMs>=0&&i-a.atMs<3e4&&n?.force!==!0||(await e.thread.startTyping(r),e.state.threadStatus={atMs:i,text:r})}async function restoreStatus(e){let t=e.state.threadStatus;t&&await showStatus(e,t.text,{force:!0})}async function hideStatus(e){let t=e.state.threadStatus;t&&(e.state.threadStatus={...t,atMs:0}),await e.thread.startTyping()}async function clearStatus(e){e.state.threadStatus=null,await e.thread.startTyping()}const HEADING=/(?:^|\n)[ \t]*\*\*([^*\n]+)\*\*/gu,SENTENCE_BOUNDARY=/(?<=[.!?])(?<!(?:^|\s)\d{1,2}\.)\s+(?=[\p{Lu}"'`(])/u,LIST_MARKER=/^(?:[-*+]|\d{1,2}[.)])\s+/u;function reasoningStatus(e,t){let n=[...e.matchAll(HEADING)].findLast(e=>e[1].trim()!==``);if(n)return n[1].trim();let r=e.split(/\r?\n/u).flatMap(e=>e.split(SENTENCE_BOUNDARY)).map(e=>e.trim().replace(LIST_MARKER,``)),i=t?.complete===!0?-1:r.length-1;for(let e=r.length-1;e>=0;--e){let t=r[e],n=e===i?SLACK_TYPING_STATUS_MAX_LENGTH:12;if(t.length>=n&&isProse(t))return t}}function isProse(e){return!e.startsWith("```")&&/\p{L}/u.test(e)}export{clearStatus,hideStatus,reasoningStatus,restoreStatus,showStatus};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import{extractErrorId,formatErrorHint}from"#internal/logging.js";function defaultTwilioAuth(e){let t={channel:e.channel,from:e.from};return e.to!==void 0&&(t.to=e.to),{attributes:t,authenticator:`twilio-webhook`,issuer:`twilio`,principalId:`twilio:${e.from}`,principalType:`user`}}function defaultOnText(e,t){return{auth:defaultTwilioAuth({channel:`text`,from:t.from,to:t.to})}}function defaultOnVoice(e,t){return{}}function defaultOnVoiceTranscription(e,t){return{auth:defaultTwilioAuth({channel:`voice`,from:t.from,to:t.to})}}const defaultEvents={async"message.completed"(e,t,n){e.finishReason!==`tool-calls`&&e.message&&await t.twilio.sendMessage(e.message)},async"turn.failed"(e,t,n){let r=formatErrorHint(e),i=extractErrorId(e.details);await t.twilio.sendMessage([`I hit an error while handling your request${r}.`,``,`Please try again, rephrase, or reach out if it keeps failing.`,...i?[``,`Error id: ${i}`]:[]].join(`
|
|
1
|
+
import{extractErrorId,formatErrorHint}from"#internal/logging.js";import{renderTextInputRequest}from"#channel/resolve-text.js";function defaultTwilioAuth(e){let t={channel:e.channel,from:e.from};return e.to!==void 0&&(t.to=e.to),{attributes:t,authenticator:`twilio-webhook`,issuer:`twilio`,principalId:`twilio:${e.from}`,principalType:`user`}}function defaultOnText(e,t){return{auth:defaultTwilioAuth({channel:`text`,from:t.from,to:t.to})}}function defaultOnVoice(e,t){return{}}function defaultOnVoiceTranscription(e,t){return{auth:defaultTwilioAuth({channel:`voice`,from:t.from,to:t.to})}}const defaultEvents={async"message.completed"(e,t,n){e.finishReason!==`tool-calls`&&e.message&&await t.twilio.sendMessage(e.message)},async"input.requested"(e,t,n){e.requests.length!==0&&await t.twilio.sendMessage(renderTwilioInputRequests(e.requests))},async"turn.failed"(e,t,n){let r=formatErrorHint(e),i=extractErrorId(e.details);await t.twilio.sendMessage([`I hit an error while handling your request${r}.`,``,`Please try again, rephrase, or reach out if it keeps failing.`,...i?[``,`Error id: ${i}`]:[]].join(`
|
|
2
2
|
`))},async"session.failed"(e,t){let n=formatErrorHint(e),r=extractErrorId(e.details);await t.twilio.sendMessage([`This session could not recover from an error${n}.`,``,`Start a new message to continue.`,...r?[``,`Error id: ${r}`]:[]].join(`
|
|
3
|
-
`))}};
|
|
3
|
+
`))}};function renderTwilioInputRequests(e){let t=e.map(renderTextInputRequest);if(e.some(e=>(e.options??[]).length>0)){let n=e.some(e=>e.allowFreeform===!0);t.push([n?`Reply with a number, or with your own answer.`:`Reply with a number to choose.`,...e.length>1?[`Your reply answers each of these.`]:[]].join(` `))}return t.join(`
|
|
4
|
+
|
|
5
|
+
`)}export{defaultEvents,defaultOnText,defaultOnVoice,defaultOnVoiceTranscription};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{pinnedNodeEngineMajor}from"../../node-engine.js";import{parseChatGptModelSelection}from"../../../shared/chatgpt-model.js";import{SUPPORTED_AUTHORED_MODULE_FILE_EXTENSIONS}from"../update/module-files.js";import{pathExists,writeTextFile}from"../files.js";import{blockingCreateInPlaceEntries}from"../create-in-place.js";import{DEFAULT_CONNECT_PACKAGE_VERSION,resolveVersionToken}from"../version-tokens.js";import{applyPackageManagerWorkspaceConfiguration,isPackageManagerWorkspaceMember,patchWorkspaceRootPackageJson}from"../workspace-root.js";import{WEB_CHANNEL_TEMPLATES}from"./web-template.js";import{AGENT_INSTRUCTIONS_TEMPLATE}from"./instructions-template.js";import{SCAFFOLDED_AGENT_PATHS}from"./agent-paths.js";import{mkdir,readdir,stat}from"node:fs/promises";import{basename,join,resolve}from"node:path";const CURRENT_DIRECTORY_PROJECT_NAME=`.`,DEFAULT_AI_PACKAGE_VERSION=`^7.0.105`,DEFAULT_ZOD_PACKAGE_VERSION=`4.5.4`,DEFAULT_EVE_PACKAGE_CONTRACT={version:`0.70.
|
|
1
|
+
import{pinnedNodeEngineMajor}from"../../node-engine.js";import{parseChatGptModelSelection}from"../../../shared/chatgpt-model.js";import{SUPPORTED_AUTHORED_MODULE_FILE_EXTENSIONS}from"../update/module-files.js";import{pathExists,writeTextFile}from"../files.js";import{blockingCreateInPlaceEntries}from"../create-in-place.js";import{DEFAULT_CONNECT_PACKAGE_VERSION,resolveVersionToken}from"../version-tokens.js";import{applyPackageManagerWorkspaceConfiguration,isPackageManagerWorkspaceMember,patchWorkspaceRootPackageJson}from"../workspace-root.js";import{WEB_CHANNEL_TEMPLATES}from"./web-template.js";import{AGENT_INSTRUCTIONS_TEMPLATE}from"./instructions-template.js";import{SCAFFOLDED_AGENT_PATHS}from"./agent-paths.js";import{mkdir,readdir,stat}from"node:fs/promises";import{basename,join,resolve}from"node:path";const CURRENT_DIRECTORY_PROJECT_NAME=`.`,DEFAULT_AI_PACKAGE_VERSION=`^7.0.105`,DEFAULT_ZOD_PACKAGE_VERSION=`4.5.4`,DEFAULT_EVE_PACKAGE_CONTRACT={version:`0.70.2`,nodeEngine:`>=24`};function resolveEvePackageContract(e=DEFAULT_EVE_PACKAGE_CONTRACT){return{version:resolveVersionToken(`evePackage.version`,e.version),nodeEngine:resolveVersionToken(`evePackage.nodeEngine`,e.nodeEngine)}}function modelProviderSlug(e){let t=(e.split(`/`)[0]??``).replaceAll(/[^A-Za-z0-9._-]/gu,``);return t.length>0?t:`anthropic`}function byokProviderEnvVar(e){let t=modelProviderSlug(e).toUpperCase().replaceAll(/[^A-Z0-9]/gu,`_`);return`${/^[0-9]/.test(t)?`_`:``}${t}_API_KEY`}function agentTemplateFiles(e,t){return{[SCAFFOLDED_AGENT_PATHS.config]:renderAgentTemplate(e,t),[SCAFFOLDED_AGENT_PATHS.channel]:WEB_CHANNEL_TEMPLATES.default,[SCAFFOLDED_AGENT_PATHS.instructions]:AGENT_INSTRUCTIONS_TEMPLATE}}function renderAgentTemplate(e,t){let n=parseChatGptModelSelection(e);return n===void 0?BASE_AGENT_TEMPLATE.replaceAll(`__EVE_INIT_MODEL__`,e).replaceAll(`__EVE_INIT_REASONING__`,reasoningTemplateLine(t)):`import { defineAgent } from "eve";\nimport { chatgpt } from "eve/models/openai";\n\nexport default defineAgent({\n model: chatgpt(${JSON.stringify(n)}),\n${reasoningTemplateLine(t)}});\n`}function reasoningTemplateLine(e){return e===void 0||e===`provider-default`?``:` reasoning: "${e}",\n`}function renderTemplate(e,t){return e===BASE_AGENT_TEMPLATE&&parseChatGptModelSelection(t.model)!==void 0?renderAgentTemplate(t.model,t.reasoning):e.replaceAll(`__EVE_INIT_APP_NAME__`,t.appName).replaceAll(`__EVE_INIT_MODEL__`,t.model).replaceAll(`__EVE_INIT_REASONING__`,reasoningTemplateLine(t.reasoning)).replaceAll(`__EVE_INIT_BYOK_PROVIDER__`,modelProviderSlug(t.model)).replaceAll(`__EVE_INIT_BYOK_ENV_VAR__`,byokProviderEnvVar(t.model)).replaceAll(`__EVE_INIT_PACKAGE_VERSION__`,formatEveDependencySpecifier(t.eveVersion)).replaceAll(`__EVE_INIT_AI_SDK_VERSION__`,t.aiPackageVersion).replaceAll(`__EVE_INIT_CONNECT_VERSION__`,t.connectPackageVersion).replaceAll(`__EVE_INIT_ZOD_VERSION__`,t.zodPackageVersion).replaceAll(`__EVE_INIT_TYPESCRIPT_VERSION__`,t.typescriptPackageVersion).replaceAll(`__EVE_INIT_TYPES_NODE_VERSION__`,t.nodeTypesVersion).replaceAll(`__EVE_INIT_NODE_ENGINE__`,t.nodeEngine)}function formatEveDependencySpecifier(e){return/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z-.]+)?$/.test(e)?`^${e}`:e}const BASE_AGENT_TEMPLATE=`import { defineAgent } from "eve";
|
|
2
2
|
|
|
3
3
|
export default defineAgent({
|
|
4
4
|
model: "__EVE_INIT_MODEL__",
|
|
@@ -67,8 +67,8 @@ export interface RoutedDeliverPayload {
|
|
|
67
67
|
*
|
|
68
68
|
* With `resolveMessage`, a plain-text message is also resolved against pending
|
|
69
69
|
* `ctx.ask()` questions: when exactly one question is pending, a matching option or
|
|
70
|
-
* permitted free text answers it and consumes the message
|
|
71
|
-
* message stays with the parent.
|
|
70
|
+
* permitted free text answers it and consumes the message along with its
|
|
71
|
+
* `context`. Otherwise the message stays with the parent.
|
|
72
72
|
*/
|
|
73
73
|
export declare function routeDeliverPayload(input: {
|
|
74
74
|
readonly allowRoute?: (requestId: string, route: ProxyInputRequest) => boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createInputRequestedEvent,createTurnWaitingEvent}from"#protocol/message.js";import{resolveTextToResponse}from"#channel/resolve-text.js";import{getHarnessEmissionState}from"#harness/emission.js";import{getSessionUsage}from"#harness/turn-tag-state.js";import{getProxyInputRequests,toProxyInputRequestEntries}from"#harness/proxy-input-requests.js";import{resolveInputOutcome}from"#harness/input-request-resolution.js";import{SESSION_LIMIT_STOP_OPTION_ID}from"#harness/session-limit-continuation.js";async function emitProxiedInputRequest(e){return await e.emit(createInputRequestedEvent({requests:e.hookPayload.event.requests,sequence:e.hookPayload.event.sequence,stepIndex:e.hookPayload.event.stepIndex,taskId:e.hookPayload.event.taskId,turnId:e.hookPayload.event.turnId})),await emitTurnWaiting(e.emit,e.session),toProxyInputRequestEntries(e.hookPayload)}async function emitProxiedAuthorizationEvent(e){await e.emit(e.hookPayload.event),e.hookPayload.event.type===`authorization.required`&&await emitTurnWaiting(e.emit,e.session)}async function emitTurnWaiting(e,t){let n=getHarnessEmissionState(t.state);await e(createTurnWaitingEvent({on:`input`,sequence:n.sequence,turnId:n.turnId,usage:getSessionUsage(t)}))}function routeDeliverPayload(e){let t=getProxyInputRequests(e.state),routable=(t,n)=>n!==void 0&&e.allowRoute?.(t,n)!==!1,n=resolveMessageAgainstQuestions({enabled:e.resolveMessage===!0,entries:t,payload:e.payload,routable}),r=[...e.payload.inputResponses??[],...n.responses],i=new Map,a=[],o,bucketFor=e=>{let t=JSON.stringify([e.childContinuationToken,e.childSessionInbox?.sessionId??``,e.remote?.sessionId??``,e.event.sequence,e.event.stepIndex,e.event.turnId,e.inputSource??null]),n=i.get(t);if(n!==void 0)return n;let r={childContinuationToken:e.childContinuationToken,remote:e.remote,event:e.event,parentRequestIds:[],responses:[],routes:[],...e.childSessionInbox!==void 0&&{childSessionInbox:e.childSessionInbox},...e.workflowAsk!==void 0&&{workflowAsk:e.workflowAsk}};return i.set(t,r),r},s=new Set;for(let e of r){let n=t.get(e.requestId);if(n===void 0||!routable(e.requestId,n)){a.push(e);continue}if(s.has(e.requestId))continue;s.add(e.requestId),n.kind===`session-limit`&&e.optionId===SESSION_LIMIT_STOP_OPTION_ID&&(o={kind:`cancel-turn`});let r=bucketFor(n);r.parentRequestIds.push(e.requestId),r.responses.push(e),r.routes.push(n)}let c=[...i.values()].map(({workflowAsk:e,remote:n,childContinuationToken:r,childSessionInbox:i,event:a,parentRequestIds:o,responses:s,routes:c})=>{let l=new Set(o),u=new Set(l);for(let e of c)if(e.batch!==void 0&&batchResolves({batch:e.batch,childContinuationToken:r,entries:t,responseIds:l}))for(let t of e.batch.requestIds)u.add(t);return{childContinuationToken:r,payload:{inputResponses:s},resolved:{event:a,resolutions:resolveRetiredRequests({entries:t,responses:s,retireRequestIds:u})},...i!==void 0&&{childSessionInbox:i},...e!==void 0&&{workflowAsk:e},...n!==void 0&&{remote:n},...c[0]?.inputSource!==void 0&&{inputSource:c[0].inputSource}}}),l={};for(let[t,r]of Object.entries(e.payload))t!==`inputResponses`&&r!==void 0&&(
|
|
1
|
+
import{inputTextKey,readAnswerText}from"#internal/input-text.js";import{createInputRequestedEvent,createTurnWaitingEvent}from"#protocol/message.js";import{resolveTextToResponse}from"#channel/resolve-text.js";import{getHarnessEmissionState}from"#harness/emission.js";import{getSessionUsage}from"#harness/turn-tag-state.js";import{getProxyInputRequests,toProxyInputRequestEntries}from"#harness/proxy-input-requests.js";import{resolveInputOutcome}from"#harness/input-request-resolution.js";import{SESSION_LIMIT_STOP_OPTION_ID}from"#harness/session-limit-continuation.js";async function emitProxiedInputRequest(e){return await e.emit(createInputRequestedEvent({requests:e.hookPayload.event.requests,sequence:e.hookPayload.event.sequence,stepIndex:e.hookPayload.event.stepIndex,taskId:e.hookPayload.event.taskId,turnId:e.hookPayload.event.turnId})),await emitTurnWaiting(e.emit,e.session),toProxyInputRequestEntries(e.hookPayload)}async function emitProxiedAuthorizationEvent(e){await e.emit(e.hookPayload.event),e.hookPayload.event.type===`authorization.required`&&await emitTurnWaiting(e.emit,e.session)}async function emitTurnWaiting(e,t){let n=getHarnessEmissionState(t.state);await e(createTurnWaitingEvent({on:`input`,sequence:n.sequence,turnId:n.turnId,usage:getSessionUsage(t)}))}const CONSUMED_MESSAGE_KEYS=new Set([`context`,`message`,inputTextKey]);function routeDeliverPayload(e){let t=getProxyInputRequests(e.state),routable=(t,n)=>n!==void 0&&e.allowRoute?.(t,n)!==!1,n=resolveMessageAgainstQuestions({enabled:e.resolveMessage===!0,entries:t,payload:e.payload,routable}),r=[...e.payload.inputResponses??[],...n.responses],i=new Map,a=[],o,bucketFor=e=>{let t=JSON.stringify([e.childContinuationToken,e.childSessionInbox?.sessionId??``,e.remote?.sessionId??``,e.event.sequence,e.event.stepIndex,e.event.turnId,e.inputSource??null]),n=i.get(t);if(n!==void 0)return n;let r={childContinuationToken:e.childContinuationToken,remote:e.remote,event:e.event,parentRequestIds:[],responses:[],routes:[],...e.childSessionInbox!==void 0&&{childSessionInbox:e.childSessionInbox},...e.workflowAsk!==void 0&&{workflowAsk:e.workflowAsk}};return i.set(t,r),r},s=new Set;for(let e of r){let n=t.get(e.requestId);if(n===void 0||!routable(e.requestId,n)){a.push(e);continue}if(s.has(e.requestId))continue;s.add(e.requestId),n.kind===`session-limit`&&e.optionId===SESSION_LIMIT_STOP_OPTION_ID&&(o={kind:`cancel-turn`});let r=bucketFor(n);r.parentRequestIds.push(e.requestId),r.responses.push(e),r.routes.push(n)}let c=[...i.values()].map(({workflowAsk:e,remote:n,childContinuationToken:r,childSessionInbox:i,event:a,parentRequestIds:o,responses:s,routes:c})=>{let l=new Set(o),u=new Set(l);for(let e of c)if(e.batch!==void 0&&batchResolves({batch:e.batch,childContinuationToken:r,entries:t,responseIds:l}))for(let t of e.batch.requestIds)u.add(t);return{childContinuationToken:r,payload:{inputResponses:s},resolved:{event:a,resolutions:resolveRetiredRequests({entries:t,responses:s,retireRequestIds:u})},...i!==void 0&&{childSessionInbox:i},...e!==void 0&&{workflowAsk:e},...n!==void 0&&{remote:n},...c[0]?.inputSource!==void 0&&{inputSource:c[0].inputSource}}}),l={};for(let[t,r]of Object.entries(e.payload))t!==`inputResponses`&&r!==void 0&&(n.consumed&&CONSUMED_MESSAGE_KEYS.has(t)||(l[t]=r));return a.length>0&&(l.inputResponses=a),{forChildren:c,forSelf:Object.keys(l).length>0?l:void 0,parentAction:o}}function resolveRetiredRequests(e){let t=new Map(e.responses.map(e=>[e.requestId,e])),n=[];for(let r of e.retireRequestIds){let i=e.entries.get(r);i!==void 0&&n.push(toInputResolution(r,i,t.get(r)))}return n}function toInputResolution(e,t,n){let r=resolveInputOutcome(t.kind,n),i={kind:t.kind,outcome:r,requestId:e};return n===void 0?i:{...i,response:n}}function resolveMessageAgainstQuestions(e){let t={consumed:!1,responses:[]};if(!e.enabled||(e.payload.inputResponses?.length??0)>0||e.payload.message===void 0)return t;let n=[...e.entries].filter(([t,n])=>n.kind===`question`&&e.routable(t,n)),r=n.flatMap(([e,t])=>{let n=t.workflowAsk?.question??t.question;return n===void 0?[]:[{requestId:e,...n}]});if(r.length===0)return t;let[i]=r,a=readAnswerText(e.payload),o=n.length===1&&i!==void 0&&a!==void 0?resolveTextToResponse(a,i):void 0;return o===void 0?t:{consumed:!0,responses:[o]}}function batchResolves(e){return e.batch.approvalRequestIds.length===0||e.batch.approvalRequestIds.every(t=>{let n=e.entries.get(t);return n===void 0||!sameBatch(n,e)||e.responseIds.has(t)})}function sameBatch(e,t){return e.childContinuationToken===t.childContinuationToken&&e.batch?.requestIds.length===t.batch.requestIds.length&&e.batch.requestIds.every((e,n)=>e===t.batch.requestIds[n])}export{emitProxiedAuthorizationEvent,emitProxiedInputRequest,routeDeliverPayload};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
async function executeAskQuestionTool(e,t){"use workflow";return toAskQuestionOutput(await t.ask(toAskQuestionRequest(e)))}function toAskQuestionRequest(e){let t=e.options?.map(e=>({description:e.description,id:e.label,label:e.label}));return{allowFreeform:!0,display:t===void 0?`text`:`select`,prompt:e.question,...t!==void 0&&{options:t}}}function toAskQuestionOutput(e){switch(e.status){case`answered`:return{answer:e.optionId??e.text??``,status:`answered`};case`cancelled`:return{interrupted:!0};case`unavailable`:return{status:`unavailable`}}}executeAskQuestionTool.workflowId=`workflow//eve@0.70.
|
|
1
|
+
async function executeAskQuestionTool(e,t){"use workflow";return toAskQuestionOutput(await t.ask(toAskQuestionRequest(e)))}function toAskQuestionRequest(e){let t=e.options?.map(e=>({description:e.description,id:e.label,label:e.label}));return{allowFreeform:!0,display:t===void 0?`text`:`select`,prompt:e.question,...t!==void 0&&{options:t}}}function toAskQuestionOutput(e){switch(e.status){case`answered`:return{answer:e.optionId??e.text??``,status:`answered`};case`cancelled`:return{interrupted:!0};case`unavailable`:return{status:`unavailable`}}}executeAskQuestionTool.workflowId=`workflow//eve@0.70.2//executeAskQuestionTool`;export{executeAskQuestionTool,toAskQuestionOutput,toAskQuestionRequest};
|
|
@@ -6,6 +6,13 @@ export interface ReadFileToolInput {
|
|
|
6
6
|
}
|
|
7
7
|
export interface ReadFileToolOutput {
|
|
8
8
|
content: string;
|
|
9
|
+
/** Set when the file is a PNG, JPEG, GIF, or WebP image the model sees as an image. */
|
|
10
|
+
image?: {
|
|
11
|
+
height?: number;
|
|
12
|
+
mediaType: string;
|
|
13
|
+
size: number;
|
|
14
|
+
width?: number;
|
|
15
|
+
};
|
|
9
16
|
nextOffset?: number;
|
|
10
17
|
path: string;
|
|
11
18
|
totalLines: number;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{defineJsonSchema}from"#tools/schema.js";import{defineTool}from"#tools/definition.js";import{toolLabel}from"#tools/tool-label.js";import{executeReadFileOnSandbox}from"#execution/sandbox/read-file.js";const READ_FILE_INPUT_SCHEMA=defineJsonSchema({type:`object`,properties:{filePath:{type:`string`,description:`The absolute path to the file to read. A leading $HOME is supported.`},limit:{type:`integer`,minimum:1,description:`Maximum number of lines to return. Defaults to 2000.`},offset:{type:`integer`,minimum:1,description:`1-based line number to start from. Defaults to 1.`}},required:[`filePath`],additionalProperties:!1}),READ_FILE_OUTPUT_SCHEMA=defineJsonSchema({type:`object`,properties:{content:{type:`string`},nextOffset:{type:`integer`,minimum:1},path:{type:`string`},totalLines:{type:`integer`,minimum:0},truncated:{type:`boolean`}},required:[`content`,`path`,`totalLines`,`truncated`],additionalProperties:!1}),readFile=defineTool({label:{start:e=>toolLabel(`Read`,e.filePath)},description:[`Read a file from the local filesystem. If the path does not exist, an error is returned.`,``,`Usage:`,`- The filePath parameter should be an absolute path or begin with $HOME/.`,`- By default, this tool returns up to 2000 lines from the start of the file.`,`- The offset parameter is the line number to start from (1-indexed).`,`- To read later sections, call this tool again with a larger offset.`,'- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\\n", you will receive "1: foo\\n".',`- Any line longer than 2000 characters is truncated
|
|
2
|
-
`),async execute(e,t){return await executeReadFileOnSandbox(await t.getSandbox(),e)},inputSchema:READ_FILE_INPUT_SCHEMA,outputSchema:READ_FILE_OUTPUT_SCHEMA});export{READ_FILE_INPUT_SCHEMA,READ_FILE_OUTPUT_SCHEMA,readFile as default,readFile};
|
|
1
|
+
import{contextStorage}from"#context/container.js";import{SandboxKey}from"#context/keys.js";import{basename}from"node:path";import{defineJsonSchema}from"#tools/schema.js";import{resolveAbsoluteFilePath}from"#execution/sandbox/require-sandbox.js";import{defineTool}from"#tools/definition.js";import{toolLabel}from"#tools/tool-label.js";import{toolOutput,toolOutputPart}from"#tools/model-output.js";import{executeReadFileOnSandbox}from"#execution/sandbox/read-file.js";const READ_FILE_INPUT_SCHEMA=defineJsonSchema({type:`object`,properties:{filePath:{type:`string`,description:`The absolute path to the file to read. A leading $HOME is supported.`},limit:{type:`integer`,minimum:1,description:`Maximum number of lines to return. Defaults to 2000.`},offset:{type:`integer`,minimum:1,description:`1-based line number to start from. Defaults to 1.`}},required:[`filePath`],additionalProperties:!1}),READ_FILE_OUTPUT_SCHEMA=defineJsonSchema({type:`object`,properties:{content:{type:`string`},image:{type:`object`,properties:{height:{type:`integer`,minimum:1},mediaType:{type:`string`},size:{type:`integer`,minimum:0},width:{type:`integer`,minimum:1}},required:[`mediaType`,`size`],additionalProperties:!1},nextOffset:{type:`integer`,minimum:1},path:{type:`string`},totalLines:{type:`integer`,minimum:0},truncated:{type:`boolean`}},required:[`content`,`path`,`totalLines`,`truncated`],additionalProperties:!1}),readFile=defineTool({label:{start:e=>toolLabel(`Read`,e.filePath)},description:[`Read a file from the local filesystem. If the path does not exist, an error is returned.`,``,`Usage:`,`- The filePath parameter should be an absolute path or begin with $HOME/.`,`- By default, this tool returns up to 2000 lines from the start of the file.`,`- The offset parameter is the line number to start from (1-indexed).`,`- To read later sections, call this tool again with a larger offset.`,'- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\\n", you will receive "1: foo\\n".',`- Any line longer than 2000 characters is truncated.`,"- PNG, JPEG, GIF, and WebP files up to 3 MiB are shown to you as images, including files named in `Attached file <path>` references.",`- Call this tool in parallel when you know there are multiple files you want to read.`,`- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.`].join(`
|
|
2
|
+
`),async execute(e,t){return await executeReadFileOnSandbox(await t.getSandbox(),e)},inputSchema:READ_FILE_INPUT_SCHEMA,outputSchema:READ_FILE_OUTPUT_SCHEMA,async toModelOutput(e){if(e.image===void 0)return toolOutput.json(e);let t=await contextStorage.getStore()?.get(SandboxKey)?.get(),n=t==null?null:await t.readBinaryFile({path:await resolveAbsoluteFilePath(t,e.path)});return n===null?toolOutput.text(`${e.content} The image could not be loaded.`):toolOutput.content([toolOutputPart.text(e.content),toolOutputPart.file(Buffer.from(n).toString(`base64`),{filename:basename(e.path),mediaType:e.image.mediaType})])}});export{READ_FILE_INPUT_SCHEMA,READ_FILE_OUTPUT_SCHEMA,readFile as default,readFile};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sleep}from"#compiled/@workflow/core/index.js";async function executeSleepTool(e,t){"use workflow";let n=new Promise(e=>t.abortSignal.addEventListener(`abort`,()=>e(`interrupted`),{once:!0})),r=sleep(Math.ceil(e.seconds*1e3)).then(()=>`elapsed`);return await Promise.race([r,n])===`elapsed`?{waitedSeconds:e.seconds}:{interrupted:!0}}executeSleepTool.workflowId=`workflow//eve@0.70.
|
|
1
|
+
import{sleep}from"#compiled/@workflow/core/index.js";async function executeSleepTool(e,t){"use workflow";let n=new Promise(e=>t.abortSignal.addEventListener(`abort`,()=>e(`interrupted`),{once:!0})),r=sleep(Math.ceil(e.seconds*1e3)).then(()=>`elapsed`);return await Promise.race([r,n])===`elapsed`?{waitedSeconds:e.seconds}:{interrupted:!0}}executeSleepTool.workflowId=`workflow//eve@0.70.2//executeSleepTool`;export{executeSleepTool};
|
package/docs/agent-config.md
CHANGED
|
@@ -180,8 +180,8 @@ export default defineAgent({
|
|
|
180
180
|
`sessionTimeoutMs` sets the lifetime for every session, including delegated
|
|
181
181
|
sessions. It defaults to 30 days and starts at creation. Each successful
|
|
182
182
|
deployment handoff or legacy-session import restarts the original configured
|
|
183
|
-
duration. Process restarts, ordinary messages, and failed
|
|
184
|
-
keep the existing deadline. At the deadline, eve lets an active turn settle,
|
|
183
|
+
duration. Process restarts, ordinary messages, compaction handoffs, and failed
|
|
184
|
+
or skipped handoffs keep the existing deadline. At the deadline, eve lets an active turn settle,
|
|
185
185
|
then emits `session.completed` and releases every continuation address; the next
|
|
186
186
|
qualifying channel message starts fresh. Set it to `false` to disable the
|
|
187
187
|
timeout. Expiration does not delete stored session data.
|
|
@@ -179,7 +179,7 @@ chatSdkChannel({
|
|
|
179
179
|
|
|
180
180
|
### Human-in-the-loop (HITL)
|
|
181
181
|
|
|
182
|
-
HITL prompts render as a Chat SDK `Card` with buttons. Button clicks resume the parked session automatically — the channel wires `bot.onAction` to `respond` for you. Change the action-id prefix with `inputActionPrefix` (default `eve_input:`) if your app already uses it, and provide `resolveInputAuth` to carry user or tenant auth across the resume:
|
|
182
|
+
HITL prompts render as a Chat SDK `Card` with buttons. Button clicks resume the parked session automatically — the channel wires `bot.onAction` to `respond` for you. Adapters that can't render cards, such as Photon iMessage, get fallback text with the options numbered. A reply forwarded through `send` with an option's number or label answers the pending request, so `1` or `approve` runs an approval-gated tool and `2` or `cancel` stops it. If the question allows a custom answer, or has no options, the message asks for a typed reply and any other text is taken as that answer. Change the action-id prefix with `inputActionPrefix` (default `eve_input:`) if your app already uses it, and provide `resolveInputAuth` to carry user or tenant auth across the resume:
|
|
183
183
|
|
|
184
184
|
```ts
|
|
185
185
|
chatSdkChannel({
|
package/docs/channels/slack.mdx
CHANGED
|
@@ -457,20 +457,26 @@ The bases say where a call may go. `apiBaseUrl` is the base Slack Web API method
|
|
|
457
457
|
|
|
458
458
|
eve shows an agent's progress in two places. Short work, such as thinking or a tool call, appears in the thread status and never posts a message. [Tasks](/docs/tools/tasks), which keep working while the conversation continues, appear in a task card: one message per turn that eve updates in place. Replies post as ordinary messages, so the thread holds the conversation, the task card, and nothing else.
|
|
459
459
|
|
|
460
|
-
| Moment
|
|
461
|
-
|
|
|
462
|
-
| A mention or DM arrives
|
|
463
|
-
| The message hook drops the message
|
|
464
|
-
| The turn starts
|
|
465
|
-
| The model reasons
|
|
466
|
-
| The model calls tools
|
|
467
|
-
|
|
|
468
|
-
|
|
|
469
|
-
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
460
|
+
| Moment | Thread status |
|
|
461
|
+
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
462
|
+
| A mention or DM arrives | `Thinking...`, right away, while the message hook is still deciding |
|
|
463
|
+
| The message hook drops the message | Cleared |
|
|
464
|
+
| The turn starts | `Thinking...` |
|
|
465
|
+
| The model reasons | Its latest reasoning heading, or its latest finished sentence |
|
|
466
|
+
| The model calls tools | Its narration before the calls, or the first action label, such as `Search useEveAgent`, plus `+N more` for the step's other calls |
|
|
467
|
+
| A tool reports progress | The tool's `label.delta` text |
|
|
468
|
+
| A tool finishes | The tool's `label.complete` text, when it defines one; otherwise the status stays |
|
|
469
|
+
| The model starts its next step | `Reviewing researcher's results...` after a task finishes, otherwise the same status again |
|
|
470
|
+
| The model streams a long reply | `Writing a reply...`, once the text runs past 280 characters |
|
|
471
|
+
| The turn waits on its tasks | `Waiting on researcher...`, or `Waiting on 3 tasks...` |
|
|
472
|
+
| The turn waits on an approval, answer, or sign-in | Cleared until the turn resumes, then the status it had |
|
|
473
|
+
| A reply posts | Cleared by Slack |
|
|
474
|
+
|
|
475
|
+
A model step never resets the status to a generic line. The status keeps naming the latest work until something newer replaces it, so a user watching the thread sees `Checking recent deploys`, then `Found 4 failed deploys`, then `Reading build logs`, without `Thinking...` in between.
|
|
476
|
+
|
|
477
|
+
Slack removes a thread status after two minutes without a message from the app, and clears it whenever the app posts in the thread. eve sets the current status again at each model step, after it posts a new task card, and as a tool reports progress. A tool that runs longer than two minutes without reporting progress loses its status. Report progress with [`label.delta`](/docs/tools#label-tool-activity), or run the work as a [task](/docs/tools/tasks), whose card shows it is still going.
|
|
478
|
+
|
|
479
|
+
Action labels follow the same rule as the task card titles described below. They leave out the model's own `task_wait` and `task_cancel` calls. Reasoning shows one piece at a time: a heading when the model titles its reasoning, as reasoning summaries do, otherwise the latest finished sentence. Each piece of reasoning, and each progress label, stays up for at least three seconds, so the status changes at a readable pace instead of sending a Slack request per token. Every status is cut to 50 characters at a word boundary.
|
|
474
480
|
|
|
475
481
|
Replies up to 12,000 characters render inline. Longer replies are uploaded unchanged as a Markdown snippet named `eve-response.md` with a short in-thread note, which requires the `files:write` bot scope.
|
|
476
482
|
|
|
@@ -792,7 +798,7 @@ Private file downloads resolve credentials from the session's app installation w
|
|
|
792
798
|
|
|
793
799
|
Downloads cover Slack's own file hosts plus URLs under a configured [`api.fileBaseUrl`](#point-the-channel-at-a-different-slack-api-host), and travel on `api.fetch` when one is configured.
|
|
794
800
|
|
|
795
|
-
Staged files live in the session sandbox under `/workspace/attachments`. Session history keeps a reference to that path, not a second copy of the Slack file. If the backend can no longer reattach the original sandbox, eve cannot restore the attachment bytes from history; later turns receive a missing-file result. Store files outside the sandbox when the agent must retain them independently of sandbox availability.
|
|
801
|
+
Staged files live in the session sandbox under `/workspace/.eve/attachments`. Session history keeps a reference to that path, not a second copy of the Slack file. If the backend can no longer reattach the original sandbox, eve cannot restore the attachment bytes from history; later turns receive a missing-file result. Store files outside the sandbox when the agent must retain them independently of sandbox availability.
|
|
796
802
|
|
|
797
803
|
## What to read next
|
|
798
804
|
|
package/docs/channels/twilio.mdx
CHANGED
|
@@ -62,7 +62,7 @@ The default `message.completed` handler sends the reply as SMS through Twilio's
|
|
|
62
62
|
|
|
63
63
|
### Human-in-the-loop (HITL)
|
|
64
64
|
|
|
65
|
-
SMS
|
|
65
|
+
SMS has no buttons or cards, so the default `input.requested` handler sends each question and tool approval as an SMS with its options numbered. A reply with an option's number or label answers the pending request, so `1` or `approve` runs an approval-gated tool and `2` or `cancel` stops it. If the question allows a custom answer, any other text is taken as that answer. Callers on a voice call get the question by SMS too. To render questions differently, declare your own `events["input.requested"]` handler.
|
|
66
66
|
|
|
67
67
|
### Proactive sessions
|
|
68
68
|
|
|
@@ -64,7 +64,7 @@ export default disableTool();
|
|
|
64
64
|
|
|
65
65
|
### `read_file`
|
|
66
66
|
|
|
67
|
-
`read_file` reads text files from the sandbox with line-numbered output. It accepts absolute paths and paths beginning with `$HOME/`.
|
|
67
|
+
`read_file` reads text files from the sandbox with line-numbered output, and shows PNG, JPEG, GIF, and WebP images up to 3 MiB to the model as images. It accepts absolute paths and paths beginning with `$HOME/`.
|
|
68
68
|
|
|
69
69
|
```sh
|
|
70
70
|
eve add tool/read_file
|
|
@@ -36,6 +36,10 @@ if a turn is running, eve queues it until that turn settles. A successful manual
|
|
|
36
36
|
compaction emits the same `compaction.requested` and `compaction.completed`
|
|
37
37
|
events as automatic compaction, followed by `session.waiting`.
|
|
38
38
|
|
|
39
|
+
After either kind of compaction, eve moves the idle session to a fresh workflow
|
|
40
|
+
run on the same deployment, so a long session's run history does not keep
|
|
41
|
+
growing. See [Execution model and durability](./execution-model-and-durability#compaction-handoff).
|
|
42
|
+
|
|
39
43
|
To discard model-message history instead of summarizing it, call the corresponding
|
|
40
44
|
`clear()` method on any of those handles. Clearing preserves the session identity,
|
|
41
45
|
system prompt, configured tools and skills, durable state, limits, and sandbox.
|
|
@@ -25,12 +25,18 @@ The Workflow SDK is not inherently tied to Vercel. In local development and in a
|
|
|
25
25
|
|
|
26
26
|
When a Vercel production deployment changes, a new delivery to an idle session hands the session's settled state to the exact deployment that accepted the request. This applies whether the delivery arrives through the session ID or through any channel continuation address the session has claimed. The session keeps its original ID and event stream, while the next model turn uses that deployment's current system-role instructions, model, and tools. Static user-role instructions stay pinned in the durable conversation history and are not seeded again. A successful handoff restarts the session's original configured [`sessionTimeoutMs`](../agent-config#runtime-limits) duration; disabled timeouts stay disabled. Failed or skipped handoffs keep the existing deadline.
|
|
27
27
|
|
|
28
|
-
Before taking ownership, the target deployment upgrades the checkpoint when an older eve release wrote it, then checks that no pending work remains. A deployment accepts checkpoints from eve 0.66.0 and later, so upgrading eve does not leave long-lived sessions, such as a Slack thread, on their old deployment. An older release cannot read a newer checkpoint: after a rollback, a session that already moved to the newer deployment stays there. Extension state saved before eve 0.69 moves to its mount when exactly one mount
|
|
28
|
+
Before taking ownership, the target deployment upgrades the checkpoint when an older eve release wrote it, then checks that no pending work remains. A deployment accepts checkpoints from eve 0.66.0 and later, so upgrading eve does not leave long-lived sessions, such as a Slack thread, on their old deployment. An older release cannot read a newer checkpoint: after a rollback, a session that already moved to the newer deployment stays there. Extension state saved before eve 0.69 moves to its mount when exactly one mount of that extension package defines that state. State that the target deployment no longer defines, such as a removed `defineState` key, is dropped, and the target deployment's runtime logs show a `dropping unknown context key during deserialization` warning. When two or more mounts of one extension package define that state, eve cannot choose an owner, and the session stays on its current deployment. Idle subagent sessions that eve 0.66–0.68 kept for later reuse are stopped after the handoff. Authored session state remains opaque to this validation.
|
|
29
29
|
|
|
30
30
|
If the target deployment rejects the session, the previous owner keeps it and processes the triggering delivery. When the target deployment cannot read the checkpoint, its Vercel runtime logs show a `session handoff refused` warning with the checkpoint version and the reason. Owners on this release or later also write a `session handoff failed` warning to their own runtime logs, with the target deployment and the reason.
|
|
31
31
|
|
|
32
32
|
eve does not move live work between deployments. A session with a running turn, pending human input or authorization, queued messages, or active tasks and subagents stays on its current deployment. After that work settles, a later delivery can move the idle session; the earlier deployment signal is not remembered.
|
|
33
33
|
|
|
34
|
+
### Compaction handoff
|
|
35
|
+
|
|
36
|
+
Every workflow run records each step it executes, so a session that ran on one run for weeks would carry its whole history in that run. To bound this, eve moves a session to a fresh run on its current deployment after [compaction](./default-harness#compaction) rewrites the conversation history. This applies to automatic compaction during a turn and to a client or channel compaction request.
|
|
37
|
+
|
|
38
|
+
The move happens once the turn settles and nothing else is waiting: no queued messages, running tasks or subagents, or pending human input. The new run receives the compacted history and settled state, then waits for the next message. If a single message reaches an otherwise idle session before the move completes, eve moves the session with that message, and the message's turn runs on the new run. If work is still pending, eve tries again after a later turn settles. The session keeps its ID, event stream, continuation addresses, and deadline; compaction never extends [`sessionTimeoutMs`](../agent-config#runtime-limits). If the new run cannot take ownership, the current run keeps the session, writes a `session handoff failed` warning to its runtime logs, and tries again later.
|
|
39
|
+
|
|
34
40
|
### Upgrading sessions from the former execution model
|
|
35
41
|
|
|
36
42
|
You can upgrade directly from a release that used a session driver and separate
|
|
@@ -12,8 +12,8 @@ controls, and streams. Every operation targets that exact session; none follows
|
|
|
12
12
|
or creates a replacement implicitly.
|
|
13
13
|
|
|
14
14
|
The session ID currently identifies the original Workflow run that owns the
|
|
15
|
-
event stream. A deployment handoff changes the executing run, not
|
|
16
|
-
ID or stream. Stream namespaces belong to a run; eve does not support
|
|
15
|
+
event stream. A deployment or compaction handoff changes the executing run, not
|
|
16
|
+
the session ID or stream. Stream namespaces belong to a run; eve does not support
|
|
17
17
|
caller-assigned session IDs or globally addressed streams.
|
|
18
18
|
|
|
19
19
|
Authored channels also have channel-local continuation tokens. A token addresses
|
|
@@ -24,7 +24,7 @@ by the eve HTTP session API. See [Custom channels](../channels/custom#channel-op
|
|
|
24
24
|
Sessions last 30 days by default; configure `limits.sessionTimeoutMs` in
|
|
25
25
|
`agent.ts`, or set it to `false` to disable the deadline. A successful deployment
|
|
26
26
|
handoff or legacy-session import restarts the original configured duration.
|
|
27
|
-
Ordinary messages and process restarts keep the existing deadline. At expiration, eve
|
|
27
|
+
Ordinary messages, compaction handoffs, and process restarts keep the existing deadline. At expiration, eve
|
|
28
28
|
lets an active turn settle, emits `session.completed`, and releases the
|
|
29
29
|
session's continuation addresses so the next qualifying channel message starts fresh. Stored
|
|
30
30
|
session data is not deleted. See [Agent config](../agent-config#runtime-limits).
|
package/docs/evals/cases.mdx
CHANGED
|
@@ -110,6 +110,7 @@ Every `t.session()` or `t.send()` creates a new independent conversation. Contin
|
|
|
110
110
|
- `session.send(message, options?)` sends a follow-up on that session and waits for it to settle.
|
|
111
111
|
- `session.start(message, options?)` starts a text turn and returns as soon as the server accepts it. The live turn exposes `.session`, `.sessionId`, `.waitForEvent(...)`, `.cancel()`, and `.result()`.
|
|
112
112
|
- `session.cancel()` requests cooperative cancellation of that session's active turn. Both `accepted` and `no_active_turn` are successful outcomes.
|
|
113
|
+
- `session.compact()` compacts that session's history between turns and waits for it to finish. It resolves to the captured events from `compaction.requested` through `session.waiting`, so assertions such as `.event("compaction.completed")` work on the result. It throws when the session is no longer active.
|
|
113
114
|
- `session.sendFile(text, path, mediaType?)` attaches a local file as a data URL.
|
|
114
115
|
- `session.agent(started).stream(options?)` follows a delegated child's stream through the parent with the eval client's credentials. Pass an `agent.started` event from that session. It also reaches children on another deployment, which `t.target.watchTurn(...)` cannot address.
|
|
115
116
|
- `session.requireInputRequest(filter?)` records a gate, requires exactly one pending request, and returns it. Filters match tool name, action input, prompt, display, and option ids.
|
package/docs/extensions.md
CHANGED
|
@@ -210,11 +210,13 @@ Extension state belongs to the logical mount path (for example, `extensions/crm`
|
|
|
210
210
|
|
|
211
211
|
### Upgrade from package-scoped extension state
|
|
212
212
|
|
|
213
|
-
Deployments before eve 0.69 stored extension state under package-prefixed keys, such as `acme-crm.requests`. When a session from one of those deployments hands off to a newer deployment, eve moves each package-prefixed value to the mount that uses that package.
|
|
213
|
+
Deployments before eve 0.69 stored extension state under package-prefixed keys, such as `acme-crm.requests`. When a session from one of those deployments hands off to a newer deployment, eve moves each package-prefixed value to the mount that uses that package.
|
|
214
214
|
|
|
215
|
-
- The value moves
|
|
215
|
+
- The value moves when exactly one mount of that package defines that state name.
|
|
216
|
+
- When no mount of that package defines that state name, for example because the extension removed it or the package was renamed, eve drops the value. The target deployment's runtime logs show a `dropping unknown context key during deserialization` warning with the key.
|
|
217
|
+
- When two or more mounts use the package and define that state name, eve cannot tell which mount owns the value, so the handoff is rejected and the session stays on its current deployment. Keep that deployment available until the session finishes, or start a new session on the deployment you want to use.
|
|
216
218
|
- Older deployments cannot read sessions saved by a newer release, so a session that already moved does not hand back after a rollback.
|
|
217
|
-
- Local context snapshots follow the same
|
|
219
|
+
- Local context snapshots follow the same rules.
|
|
218
220
|
|
|
219
221
|
### Use an extension in a workspace
|
|
220
222
|
|