@sentry/junior 0.240.0 → 0.242.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{admin-D6fRPRgC.js → admin-C54kvHVC.js} +1 -1
- package/dist/{agent-hooks-JFbgedMZ.js → agent-hooks-BVttjZvb.js} +1952 -1952
- package/dist/{agent-runner-0IjJxsL1.js → agent-runner-BTgnxJ7g.js} +336 -1125
- package/dist/api/schema.d.ts +9 -0
- package/dist/api/schema.js +2 -2
- package/dist/api.js +21 -21
- package/dist/app.js +104 -31
- package/dist/{automation-B5_c8hEb.js → automation-gznOMaQ2.js} +11 -2
- package/dist/{briefs-BGWTp-nz.js → briefs-CKUsv9F1.js} +2 -2
- package/dist/{catalog-runtime-DdigNl7s.js → catalog-runtime-C2YkfMZz.js} +1 -1
- package/dist/cli/chat.js +12 -12
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +2 -2
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +3 -3
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-Cn73E7br.js → client-D51G-Jdg.js} +2 -2
- package/dist/{config-B0S86yw9.js → config-C6Uu3Re9.js} +2 -2
- package/dist/{conversation-privacy-BKlX7D_R.js → conversation-privacy-CzL47nXs.js} +1 -1
- package/dist/{credential-sync-DeNQm7st.js → credential-sync-DvDaE-Uj.js} +14 -14
- package/dist/{executor-BQ-A5nov.js → executor-CGUukwus.js} +4 -2
- package/dist/{images-C7OKd6uy.js → images-DSxZt5vk.js} +2 -2
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-DgXLqoeB.js → job-queue-hLylM9cy.js} +1 -1
- package/dist/{logging-BSNF167N.js → logging-BdZ_mK3q.js} +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-authorization-BM7g61od.js → oauth-authorization-BokpeHv-.js} +1 -1
- package/dist/{oauth-callback-server-Dw-BwI6x.js → oauth-callback-server-C3BOSmT_.js} +2 -2
- package/dist/{pending-auth-Chn_EA1_.js → pending-auth-DOzL4D24.js} +2 -2
- package/dist/{profile-3t9H_IwL.js → profile-2gPzzMib.js} +4 -4
- package/dist/{registry-DLZlAhdP.js → registry-XJkXDNK2.js} +1 -1
- package/dist/{resolve-9_cIPQsq.js → resolve-DhcdtDt6.js} +2 -2
- package/dist/{runner-C_eUbFyp.js → runner-CGyrnIPm.js} +9 -9
- package/dist/{sandbox-egress-signals-C1ycxTNA.js → sandbox-egress-signals-D6F3OZJP.js} +1 -1
- package/dist/{schema-C4XwHTY4.js → schema-Cabm4jcj.js} +1 -1
- package/dist/{skills-Dqgjr_Tl.js → skills-C6PqcBPU.js} +2 -2
- package/dist/{spawn-BfEr3t7b.js → spawn-DL9x3T1o.js} +2 -2
- package/dist/{task-queue-CuFZDOXn.js → task-queue-vZnBGu9B.js} +3 -3
- package/dist/{task-runner-c7gwV8dj.js → task-runner-C-c0t5F5.js} +54 -12
- package/dist/{checkpoint-C1ZnSz7r.js → tasks-EqCtmUJ2.js} +1056 -14
- package/dist/{thread-state-exTu-G1w.js → thread-state-B8IKs_J8.js} +6 -6
- package/dist/{turn-failure-response-DFp99ziF.js → turn-failure-response-BS7OO1BE.js} +9 -8
- package/dist/{turn-lifecycle-C4TjuOzG.js → turn-lifecycle-Cxrt8qMD.js} +1 -1
- package/dist/{turn-session-routing-Vpg8jUD1.js → turn-session-routing-BlGt2AUb.js} +1 -1
- package/dist/{validation-D2lULEG9.js → validation-C8hci6pj.js} +1 -1
- package/dist/{version-BjBMCc2o.js → version-CjnHQ8aY.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-XE3nFqKM.js → viewer-BOgEy5zC.js} +3 -3
- package/dist/{web-authorization-8E8IrWOj.js → web-authorization-DSMV5tMK.js} +6 -6
- package/dist/{web-input-DF4GGJrf.js → web-input-TD5P9jdo.js} +135 -129
- package/dist/{work-Dt_JjfbX.js → work-D39OAwP1.js} +7 -7
- package/dist/{worker-BrbnLmD3.js → worker-CWiLgCWH.js} +5 -5
- package/migrations/0049_event_automation_status_reason.sql +1 -0
- package/migrations/meta/0049_snapshot.json +3581 -0
- package/migrations/meta/_journal.json +8 -1
- package/package.json +5 -5
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
import { b as getGatewayApiKey, c as hasCompactedConversationContext, d as unwrapCurrentInstruction, f as escapeXml, g as getProviderErrorAttributes, h as findProviderError, i as completeText, l as isCompactionSummaryText, m as createProviderError, n as GEN_AI_SERVER_ADDRESS, o as resolveGatewayModel, p as ProviderError, r as completeObject, s as COMPACTION_SUMMARY_PREFIX, t as GEN_AI_PROVIDER_NAME, u as renderCurrentInstruction, v as isProviderRetryError, x as resolveGatewayCredential, y as MISSING_GATEWAY_CREDENTIALS_ERROR } from "./client-
|
|
2
|
-
import { C as withLogContext, F as normalizeIdentityEmail, I as isRecord$3, L as toOptionalNumber, R as toOptionalString, a as extractGenAiUsageSummary, b as setTags, c as getLogContextAttributesForSpan, d as logException, f as logInfo, g as serializeGenAiAttribute, i as extractGenAiUsageAttributes, l as getTracePropagationHeaders, m as normalizeGenAiFinishReason, p as logWarn,
|
|
3
|
-
import { m as startInactiveSpan,
|
|
4
|
-
import { a as toCanonicalInputMessage, c as toGenAiPayloadMetadata, i as runWithConversationPrivacy, l as toGenAiPayloadTraceAttributes, o as toCanonicalOutputMessage, r as resolveConversationPrivacy, s as toGenAiMessagesTraceAttributes, t as canExposeConversationPayload } from "./conversation-privacy-
|
|
5
|
-
import { i as getChatConfig, n as botConfig, v as normalizeSlackEmojiName } from "./config-
|
|
1
|
+
import { b as getGatewayApiKey, c as hasCompactedConversationContext, d as unwrapCurrentInstruction, f as escapeXml, g as getProviderErrorAttributes, h as findProviderError, i as completeText, l as isCompactionSummaryText, m as createProviderError, n as GEN_AI_SERVER_ADDRESS, o as resolveGatewayModel, p as ProviderError, r as completeObject, s as COMPACTION_SUMMARY_PREFIX, t as GEN_AI_PROVIDER_NAME, u as renderCurrentInstruction, v as isProviderRetryError, x as resolveGatewayCredential, y as MISSING_GATEWAY_CREDENTIALS_ERROR } from "./client-D51G-Jdg.js";
|
|
2
|
+
import { C as withLogContext, F as normalizeIdentityEmail, I as isRecord$3, L as toOptionalNumber, R as toOptionalString, a as extractGenAiUsageSummary, b as setTags, c as getLogContextAttributesForSpan, d as logException, f as logInfo, g as serializeGenAiAttribute, i as extractGenAiUsageAttributes, l as getTracePropagationHeaders, m as normalizeGenAiFinishReason, p as logWarn, u as logError, v as setSpanAttributes, w as withSpan, x as summarizeMessageText, y as setSpanStatus } from "./logging-BdZ_mK3q.js";
|
|
3
|
+
import { m as startInactiveSpan, v as withActiveSpan } from "./sentry-CgWZM_kV.js";
|
|
4
|
+
import { a as toCanonicalInputMessage, c as toGenAiPayloadMetadata, i as runWithConversationPrivacy, l as toGenAiPayloadTraceAttributes, o as toCanonicalOutputMessage, r as resolveConversationPrivacy, s as toGenAiMessagesTraceAttributes, t as canExposeConversationPayload } from "./conversation-privacy-CzL47nXs.js";
|
|
5
|
+
import { i as getChatConfig, n as botConfig, v as normalizeSlackEmojiName } from "./config-C6Uu3Re9.js";
|
|
6
6
|
import { a as formatModelProfiles, c as profileConfig, i as formatModelProfile, l as TURN_REASONING_LEVELS, n as ModelProfileNotConfiguredError, o as modelIdForProfile, s as modelProfileSchema } from "./model-profile-BwU8mqwD.js";
|
|
7
7
|
import { a as isUserActor, d as parseSlackTeamId, f as parseSlackUserId, g as parseSlackMessageTs, l as parseSlackChannelId, s as parseStoredSlackActor, u as parseSlackChannelReferenceId } from "./actor-BTtLaeWy.js";
|
|
8
|
-
import { F as sameDestination$1, N as parseDestination, T as hasAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, l as getConversationMessageSearchStore, r as readActorIdentity, u as getConversationStore, x as addAgentTurnUsage } from "./viewer-
|
|
8
|
+
import { F as sameDestination$1, N as parseDestination, T as hasAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, l as getConversationMessageSearchStore, r as readActorIdentity, u as getConversationStore, x as addAgentTurnUsage } from "./viewer-BOgEy5zC.js";
|
|
9
9
|
import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
|
|
10
|
-
import { A as juniorArtifacts, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, a as juniorSchedulerRuns, f as AGENT_INVOCATION_MAILBOX_STATUSES, l as juniorStats
|
|
10
|
+
import { A as juniorArtifacts, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, a as juniorSchedulerRuns, f as AGENT_INVOCATION_MAILBOX_STATUSES, l as juniorStats } from "./executor-CGUukwus.js";
|
|
11
11
|
import { d as instructionActors, f as instructionProvenanceFor, p as sameActorIdentity, t as KNOWN_CONVERSATION_EVENT_TYPES, u as contextProvenance } from "./history-BLTUgFYO.js";
|
|
12
12
|
import { a as listReferenceFiles, c as soulPathCandidates, l as worldPathCandidates } from "./package-discovery-BVvSnQBB.js";
|
|
13
|
-
import { a as resolveAuthTokenPlaceholder, c as fetchWithBoundedOAuthErrorBodies, d as credentialContextForActor, f as credentialContextSchema, h as CredentialUnavailableError, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, m as credentialUserSubjectId, o as resolvePluginCommandEnv, p as credentialSubjectSchema, s as OAuthProviderError } from "./registry-
|
|
14
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
15
|
-
import { a as parseSkillInvocation, i as parseSkillFile, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-
|
|
16
|
-
import {
|
|
17
|
-
import { A as getWorkspaceSnapshotBuilder, B as cancelWatch, Bt as
|
|
13
|
+
import { a as resolveAuthTokenPlaceholder, c as fetchWithBoundedOAuthErrorBodies, d as credentialContextForActor, f as credentialContextSchema, h as CredentialUnavailableError, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, m as credentialUserSubjectId, o as resolvePluginCommandEnv, p as credentialSubjectSchema, s as OAuthProviderError } from "./registry-XJkXDNK2.js";
|
|
14
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-C2YkfMZz.js";
|
|
15
|
+
import { a as parseSkillInvocation, i as parseSkillFile, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-C6PqcBPU.js";
|
|
16
|
+
import { A as formatSlackLink, Ct as loadConversationProjection, D as automationRevision, Dt as openConversationProjection, Et as loadTurnRoute, F as addReactionToMessage, G as loadTurnCheckpoint, Gt as fallbackShortTitle, I as getSlackMessagePermalink, It as recordToolExecutionStarted, K as saveTurnCheckpoint, Kt as resolveTaskTitle, L as openSlackDirectMessage, Lt as recordTurnRoute, Mt as recordAuthorizationRequested, N as slackMention, Nt as recordGuardianActionReviewed, Ot as recordAgentsInstructionsUpdated, Pt as recordMcpProviderConnected, R as postSlackEphemeralMessage, St as loadConnectedMcpProviders, V as uploadFilesToConversation, W as continuableMessages, _t as trimTrailingAssistantMessages, a as readScheduledAutomation, an as getAgentContextCompactionTriggerTokens, at as getPiMessageRole, b as saveEventAutomation, cn as piMessageFromHistoryItem, ct as hasRuntimeTurnContext, dt as isContinuablePiBoundary, et as TurnSliceLimitExceededError, f as createEventAutomation, ft as isToolResultError, g as getEventAutomation, gt as stripRuntimeTurnContext, ht as retainRuntimeTurnContext, in as estimateTextTokens, it as extractAssistantText, k as escapeSlackMrkdwnText, kt as recordAttachmentsDelivered, mt as normalizeToolNameFromResult, o as saveScheduledAutomation, on as getAgentContextInputLimitTokens, ot as getTerminalAssistantMessages, p as deleteEventAutomation, pt as isToolResultMessage, r as listScheduledAutomationsForTeam, rt as isTurnExecutionLimitExceededError, sn as historyItemFromPiMessage, st as getUserMessageInstructionText, t as createScheduledAutomation, tt as assertTurnToolCallLimit, ut as isAssistantMessage, v as listEventAutomationsForTeam, vt as TURN_CONTEXT_TAG, x as setEventAutomationStatus, yt as ToolInputError, z as postSlackMessage } from "./tasks-EqCtmUJ2.js";
|
|
17
|
+
import { A as getWorkspaceSnapshotBuilder, B as cancelWatch, Bt as isConversationScopedChannel, C as WorkspaceValidationError, D as setWorkspaceSnapshotBuild, E as setWorkspaceSnapshot, F as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, Ft as SlackActionError, G as findMatchingWatches, H as createTimerWatch, Ht as normalizeSlackConversationId, I as RESOURCE_SUBSCRIPTION_MAX_TTL_MS, It as downloadPrivateSlackFile, K as listWatches, L as RESOURCE_WATCH_TOOL_SOURCE, Lt as getFilePermalink, M as workspaceRepoCheckoutPath, Mt as defaultTimezone, N as resolveChannelCapabilities, Nt as resolveUserTimezone, O as SANDBOX_RUNTIME, Ot as saveObjectAnnotations, P as getSlackToolContext, R as STOP_WATCHING_TOOL_NAME, S as updateWorkspace, T as loadSnapshotsForProfile, U as createWatch, Vt as isDmChannel$1, W as deliverWatch, Wt as withSlackRetries, _ as deleteWorkspace, b as listWorkspaces, c as getPluginTools, et as verifyTimerWatchCredentialSubject, g as createWorkspace, it as getDashboardTaskLink, j as tryWorkspaceRepoCheckoutPath, k as deleteWorkspaceSnapshotBuilders, kt as createPluginAnnotations, l as getPluginUserPromptContributions, lt as createPluginLogger, nt as getDashboardConversationLink, r as createPluginHookRunner, s as getPluginSystemPromptContributions, tt as JUNIOR_THREAD_STATE_TTL_MS, u as getPlugins, v as getWorkspace, w as invalidateReadySnapshot, wt as matchSlackAttachments, y as getWorkspaceByName, z as cancelSubscriptions, zt as getSlackClient } from "./agent-hooks-BVttjZvb.js";
|
|
18
18
|
import { F as isVisionImageMediaType, G as removedCardSchema, L as briefLinkSchema, R as briefOutcomeStatusSchema, z as conversationBriefSchema } from "./conversation-DwD-vquD.js";
|
|
19
|
-
import { A as SANDBOX_DATA_ROOT, C as getDefaultRedisStateAdapterFor, D as fenceLock, E as acquireActiveLock, F as sandboxSkillFile, I as sandboxSkillPathResolution, M as SANDBOX_SKILLS_ROOT, N as SANDBOX_WORKSPACE_ROOT, O as withLock, P as sandboxSkillDir, _ as workspaceSnapshotNotReadyUserMessage, a as isAbortError, b as buildNonInteractiveShellScript, c as isSandboxCommandStreamInterruptedError, d as isSnapshottingError, f as throwSandboxOperationError, g as isWorkspaceSnapshotNotReadyError, h as getWorkspaceSnapshotNotReadyError, i as ensureDockerDaemon, j as SANDBOX_REPOS_ROOT, k as sleep$2, l as isSandboxMissingError, m as WorkspaceSnapshotNotReadyError, n as hash, o as isAlreadyExistsError, p as wrapSandboxSetupError, r as isStale, s as isSandboxApiTransientError, t as create, u as isSandboxUnavailableError, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand, y as buildCommandScript } from "./profile-
|
|
20
|
-
import { n as canReusePendingAuthLink, t as abandonReplacedPendingAuth } from "./pending-auth-
|
|
21
|
-
import { a as NO_REPLY_MARKER, c as CooperativeTurnYieldError, i as sanitizeAssistantText, l as TurnInputCommitLostError, n as decideReply, o as isNoReplyMarker, p as isTurnInputCommitLostError, r as getAssistantReplyText, t as authorizationId } from "./oauth-authorization-
|
|
19
|
+
import { A as SANDBOX_DATA_ROOT, C as getDefaultRedisStateAdapterFor, D as fenceLock, E as acquireActiveLock, F as sandboxSkillFile, I as sandboxSkillPathResolution, M as SANDBOX_SKILLS_ROOT, N as SANDBOX_WORKSPACE_ROOT, O as withLock, P as sandboxSkillDir, _ as workspaceSnapshotNotReadyUserMessage, a as isAbortError, b as buildNonInteractiveShellScript, c as isSandboxCommandStreamInterruptedError, d as isSnapshottingError, f as throwSandboxOperationError, g as isWorkspaceSnapshotNotReadyError, h as getWorkspaceSnapshotNotReadyError, i as ensureDockerDaemon, j as SANDBOX_REPOS_ROOT, k as sleep$2, l as isSandboxMissingError, m as WorkspaceSnapshotNotReadyError, n as hash, o as isAlreadyExistsError, p as wrapSandboxSetupError, r as isStale, s as isSandboxApiTransientError, t as create, u as isSandboxUnavailableError, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand, y as buildCommandScript } from "./profile-2gPzzMib.js";
|
|
20
|
+
import { n as canReusePendingAuthLink, t as abandonReplacedPendingAuth } from "./pending-auth-DOzL4D24.js";
|
|
21
|
+
import { a as NO_REPLY_MARKER, c as CooperativeTurnYieldError, i as sanitizeAssistantText, l as TurnInputCommitLostError, n as decideReply, o as isNoReplyMarker, p as isTurnInputCommitLostError, r as getAssistantReplyText, t as authorizationId } from "./oauth-authorization-BokpeHv-.js";
|
|
22
22
|
import { i as isValidTimeZone, r as getZonedDateTimeParts } from "./cadence-D00SCzT1.js";
|
|
23
23
|
import { r as EVENT_AUTHOR_ID } from "./prompt-CMU-B4FA.js";
|
|
24
|
-
import { a as createSandboxSession, c as postinstall, d as getSandboxResources, i as bindSandboxFileSystem, l as setSnapshotSpanAttributes, n as isMissingError, o as stopSession, r as resolve$1, s as dependencies } from "./resolve-
|
|
25
|
-
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-
|
|
26
|
-
import {
|
|
24
|
+
import { a as createSandboxSession, c as postinstall, d as getSandboxResources, i as bindSandboxFileSystem, l as setSnapshotSpanAttributes, n as isMissingError, o as stopSession, r as resolve$1, s as dependencies } from "./resolve-DhcdtDt6.js";
|
|
25
|
+
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-hLylM9cy.js";
|
|
26
|
+
import { F as taskOutcomeInputSchema, M as compileScheduleIntent, N as scheduleIntentSchema, O as automationInstructionToolSchema, T as SCHEDULED_AUTOMATION_SYSTEM_ACTOR, j as ScheduleIntentError, k as automationTitleSchema } from "./automation-gznOMaQ2.js";
|
|
27
27
|
import { r as isExperimentalFeatureEnabled } from "./experimental-Qn9rKLgx.js";
|
|
28
28
|
import { n as privateTraceResultAttributes } from "./private-trace-result-BLvBjy_e.js";
|
|
29
29
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
@@ -34,8 +34,8 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
34
34
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
35
35
|
import { z } from "zod";
|
|
36
36
|
import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
37
|
-
import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists,
|
|
38
|
-
import { EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, actorSchema, destinationSchema, eventMatchSchema,
|
|
37
|
+
import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists, or, sql } from "drizzle-orm";
|
|
38
|
+
import { EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, actorSchema, destinationSchema, eventMatchSchema, eventSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectTypeSchema, ownedObjectAnnotationSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, sourceSchema, stableEventMatchKey, watchResultSchema } from "@sentry/junior-plugin-api";
|
|
39
39
|
import { alias } from "drizzle-orm/pg-core";
|
|
40
40
|
import { Agent, estimateContextTokens } from "@earendil-works/pi-agent-core";
|
|
41
41
|
import fs from "node:fs/promises";
|
|
@@ -222,15 +222,6 @@ var SkillSandbox = class {
|
|
|
222
222
|
}
|
|
223
223
|
};
|
|
224
224
|
//#endregion
|
|
225
|
-
//#region src/chat/tools/execution/tool-input-error.ts
|
|
226
|
-
/** Thrown when a tool fails due to invalid model/user input, not a system error. */
|
|
227
|
-
var ToolInputError = class extends Error {
|
|
228
|
-
constructor(message, options) {
|
|
229
|
-
super(message, options);
|
|
230
|
-
this.name = "ToolInputError";
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
//#endregion
|
|
234
225
|
//#region src/chat/tools/sandbox/file-utils.ts
|
|
235
226
|
const MAX_TEXT_CHARS = 6e4;
|
|
236
227
|
const RIPGREP_EXCLUDED_GLOBS = ["!**/.git/**", "!**/node_modules/**"];
|
|
@@ -267,7 +258,8 @@ function isMissingPathError(error) {
|
|
|
267
258
|
}
|
|
268
259
|
/** Reject an expected missing search/list path through the tool-error channel. */
|
|
269
260
|
function missingPathSearchResult(params) {
|
|
270
|
-
|
|
261
|
+
const textPath = params.displayPath ?? params.missingPath ?? params.path;
|
|
262
|
+
throw new ToolInputError(`Path not found: ${textPath}`);
|
|
271
263
|
}
|
|
272
264
|
function escapeRegExp(value) {
|
|
273
265
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -811,10 +803,102 @@ function shouldEmitDevAgentTrace() {
|
|
|
811
803
|
return process.env.NODE_ENV === "development";
|
|
812
804
|
}
|
|
813
805
|
//#endregion
|
|
806
|
+
//#region src/chat/automation-result.ts
|
|
807
|
+
/**
|
|
808
|
+
* Automation run result.
|
|
809
|
+
*
|
|
810
|
+
* A Scheduled automation or Event automation run ends with one declared
|
|
811
|
+
* result. The run does not deliver its final assistant text. The work owner
|
|
812
|
+
* reads the declared result and applies the stored outcomes.
|
|
813
|
+
*/
|
|
814
|
+
/** Model-facing tool that ends an Automation run. */
|
|
815
|
+
const FINISH_AUTOMATION_RUN_TOOL_NAME = "finishAutomationRun";
|
|
816
|
+
/** One declared result for an Automation run. */
|
|
817
|
+
const automationResultSchema = z.discriminatedUnion("result", [
|
|
818
|
+
z.object({
|
|
819
|
+
result: z.literal("send_message"),
|
|
820
|
+
message: z.string().transform(sanitizeAssistantText).pipe(z.string().min(1))
|
|
821
|
+
}),
|
|
822
|
+
z.object({
|
|
823
|
+
result: z.literal("no_action"),
|
|
824
|
+
reason: z.string().trim().min(1)
|
|
825
|
+
}),
|
|
826
|
+
z.object({
|
|
827
|
+
result: z.literal("misconfigured"),
|
|
828
|
+
reason: z.string().trim().min(1)
|
|
829
|
+
})
|
|
830
|
+
]);
|
|
831
|
+
/**
|
|
832
|
+
* Return whether the runtime delivers the final assistant text of a run. It
|
|
833
|
+
* does not for a dispatch with no outcomes. It does not for an Automation
|
|
834
|
+
* run, which posts only its declared message, after the run.
|
|
835
|
+
*/
|
|
836
|
+
function deliversFinalText(dispatch) {
|
|
837
|
+
return dispatch?.outcomes?.length !== 0 && !dispatch?.declaresResult;
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Return the text to post after a run ends, if any. A chat Turn already
|
|
841
|
+
* delivered a successful reply, so it posts only its failure reply. An
|
|
842
|
+
* Automation run posts only a declared message. It never posts a failure
|
|
843
|
+
* reply to its outcomes. The failed execution shows on the Automation.
|
|
844
|
+
*/
|
|
845
|
+
function finishedRunReply(result, dispatch) {
|
|
846
|
+
if (!dispatch?.declaresResult) return result.diagnostics.outcome === "success" ? void 0 : result.text;
|
|
847
|
+
return result.diagnostics.outcome === "success" && result.automation?.result === "send_message" && dispatch.outcomes?.length !== 0 ? result.text : void 0;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Read the last successful declared result from agent history items.
|
|
851
|
+
*
|
|
852
|
+
* Pass the whole run history, not only the current slice. An Automation run
|
|
853
|
+
* owns its dispatch Conversation, and a resumed slice must see a result that
|
|
854
|
+
* an earlier slice already saved.
|
|
855
|
+
*/
|
|
856
|
+
function readAutomationResult(messages) {
|
|
857
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
858
|
+
const message = messages[index];
|
|
859
|
+
if (!isToolResultMessage(message) || isToolResultError(message) || normalizeToolNameFromResult(message) !== "finishAutomationRun") continue;
|
|
860
|
+
const parsed = automationResultSchema.safeParse(message.details);
|
|
861
|
+
if (parsed.success) return parsed.data;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Map one finished agent run to its dispatch outcome. A declared
|
|
866
|
+
* `misconfigured` result becomes a blocked dispatch with the declared reason.
|
|
867
|
+
*/
|
|
868
|
+
function runDispatchOutcome(result) {
|
|
869
|
+
if (result.diagnostics.outcome !== "success") return {
|
|
870
|
+
errorMessage: result.diagnostics.errorMessage ?? `Agent turn ended with ${result.diagnostics.outcome}.`,
|
|
871
|
+
outcome: "failed"
|
|
872
|
+
};
|
|
873
|
+
if (result.automation?.result === "misconfigured") return {
|
|
874
|
+
errorMessage: result.automation.reason,
|
|
875
|
+
outcome: "blocked"
|
|
876
|
+
};
|
|
877
|
+
return { outcome: "completed" };
|
|
878
|
+
}
|
|
879
|
+
const MISSING_RESULT_REMINDER = "This automation run has not ended. Your text was not delivered. Call `finishAutomationRun` now with one result.";
|
|
880
|
+
/**
|
|
881
|
+
* Return agent history plus a reminder when the model stopped without a
|
|
882
|
+
* declared result, or undefined when the run already declared one.
|
|
883
|
+
*/
|
|
884
|
+
function remindMissingAutomationResult(messages) {
|
|
885
|
+
const last = messages.at(-1);
|
|
886
|
+
if (!isAssistantMessage(last) || last.stopReason !== "stop" || readAutomationResult(messages)) return;
|
|
887
|
+
return [...messages, {
|
|
888
|
+
role: "user",
|
|
889
|
+
content: [{
|
|
890
|
+
type: "text",
|
|
891
|
+
text: MISSING_RESULT_REMINDER
|
|
892
|
+
}],
|
|
893
|
+
timestamp: Date.now()
|
|
894
|
+
}];
|
|
895
|
+
}
|
|
896
|
+
//#endregion
|
|
814
897
|
//#region src/chat/services/turn-result.ts
|
|
815
898
|
/** Process raw agent messages into a structured AgentRunResult. */
|
|
816
899
|
function buildTurnResult(input) {
|
|
817
|
-
|
|
900
|
+
if (input.requireAutomationResult) return buildAutomationTurnResult(input);
|
|
901
|
+
const { newMessages, sandboxRef, shouldTrace } = input;
|
|
818
902
|
const toolResults = newMessages.filter(isToolResultMessage);
|
|
819
903
|
const assistantMessages = newMessages.filter(isAssistantMessage);
|
|
820
904
|
const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
|
|
@@ -857,29 +941,68 @@ function buildTurnResult(input) {
|
|
|
857
941
|
"app.ai.assistant_messages": assistantMessages.length,
|
|
858
942
|
...stopReason ? { "gen_ai.response.finish_reasons": [stopReason] } : void 0
|
|
859
943
|
});
|
|
860
|
-
|
|
944
|
+
return {
|
|
945
|
+
text: primaryText,
|
|
946
|
+
sandboxRef,
|
|
947
|
+
piMessages: input.piMessages,
|
|
948
|
+
diagnostics: buildDiagnostics(input, {
|
|
949
|
+
outcome,
|
|
950
|
+
usedPrimaryText,
|
|
951
|
+
stopReason,
|
|
952
|
+
errorMessage,
|
|
953
|
+
lastAssistant
|
|
954
|
+
})
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function buildDiagnostics(input, result) {
|
|
958
|
+
const { outcome, errorMessage, lastAssistant } = result;
|
|
959
|
+
const toolResults = input.newMessages.filter(isToolResultMessage);
|
|
960
|
+
return {
|
|
861
961
|
outcome,
|
|
862
|
-
modelId,
|
|
863
|
-
assistantMessageCount:
|
|
864
|
-
reasoningLevel: executionProfile.reasoningLevel,
|
|
865
|
-
toolCalls,
|
|
962
|
+
modelId: input.modelId,
|
|
963
|
+
assistantMessageCount: input.newMessages.filter(isAssistantMessage).length,
|
|
964
|
+
reasoningLevel: input.executionProfile.reasoningLevel,
|
|
965
|
+
toolCalls: input.toolCalls,
|
|
866
966
|
toolResultCount: toolResults.length,
|
|
867
|
-
toolErrorCount,
|
|
868
|
-
usedPrimaryText,
|
|
869
|
-
durationMs,
|
|
870
|
-
usage,
|
|
871
|
-
stopReason,
|
|
967
|
+
toolErrorCount: toolResults.filter((toolResult) => toolResult.isError).length,
|
|
968
|
+
usedPrimaryText: result.usedPrimaryText,
|
|
969
|
+
durationMs: input.durationMs,
|
|
970
|
+
usage: input.usage,
|
|
971
|
+
stopReason: result.stopReason,
|
|
872
972
|
errorMessage,
|
|
873
973
|
providerError: outcome === "provider_error" && errorMessage ? createProviderError(errorMessage, {
|
|
874
|
-
modelId,
|
|
875
|
-
retryable:
|
|
974
|
+
modelId: input.modelId,
|
|
975
|
+
retryable: isAssistantMessage(lastAssistant) && isRetryableAssistantError(lastAssistant)
|
|
876
976
|
}) : void 0
|
|
877
977
|
};
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Build the result of an Automation run from its declared result. Final
|
|
981
|
+
* assistant text is not used. A run without a declared result fails.
|
|
982
|
+
*/
|
|
983
|
+
function buildAutomationTurnResult(input) {
|
|
984
|
+
const lastAssistant = input.newMessages.filter(isAssistantMessage).at(-1);
|
|
985
|
+
const stopReason = lastAssistant?.stopReason;
|
|
986
|
+
const automation = readAutomationResult(input.piMessages ?? input.newMessages);
|
|
987
|
+
const outcome = automation ? "success" : stopReason === "error" ? "provider_error" : "execution_failure";
|
|
988
|
+
const errorMessage = outcome === "provider_error" ? lastAssistant?.errorMessage : outcome === "execution_failure" ? "Automation run ended without a declared result." : void 0;
|
|
989
|
+
if (input.shouldTrace) logInfo("agent.message.generated", {
|
|
990
|
+
"app.message.kind": "automation_result",
|
|
991
|
+
"app.ai.outcome": outcome,
|
|
992
|
+
"app.automation.result": automation?.result ?? "missing"
|
|
993
|
+
});
|
|
878
994
|
return {
|
|
879
|
-
text:
|
|
880
|
-
|
|
995
|
+
text: automation?.result === "send_message" ? automation.message : "",
|
|
996
|
+
...automation ? { automation } : void 0,
|
|
997
|
+
sandboxRef: input.sandboxRef,
|
|
881
998
|
piMessages: input.piMessages,
|
|
882
|
-
diagnostics:
|
|
999
|
+
diagnostics: buildDiagnostics(input, {
|
|
1000
|
+
outcome,
|
|
1001
|
+
usedPrimaryText: automation?.result === "send_message",
|
|
1002
|
+
stopReason,
|
|
1003
|
+
errorMessage,
|
|
1004
|
+
lastAssistant
|
|
1005
|
+
})
|
|
883
1006
|
};
|
|
884
1007
|
}
|
|
885
1008
|
//#endregion
|
|
@@ -1742,11 +1865,19 @@ const SLACK_ACTION_RULES = [
|
|
|
1742
1865
|
"- Ambient reaction requests target the current inbound message; do not ask for a message reference.",
|
|
1743
1866
|
`- When no visible thread reply is requested or useful, keep tool-calling messages text-free and make the final message exactly ${NO_REPLY_MARKER}.`
|
|
1744
1867
|
];
|
|
1868
|
+
const AUTOMATION_RUN_RULES = [
|
|
1869
|
+
"- This run comes from a stored automation, not from a person. The stored instruction is the job, and its creator already approved it.",
|
|
1870
|
+
"- Nobody reads this run while it executes. Do not ask questions, ask for approval, or offer options. Infer conservatively and do the work now.",
|
|
1871
|
+
"- Check every condition in the instruction before any action with side effects. If a condition is not met or cannot be verified, stop before you act.",
|
|
1872
|
+
"- A tool result with `timed_out: true` means that attempt did not finish. Before you retry work that may have side effects, inspect authoritative state and do not repeat a mutation that already applied.",
|
|
1873
|
+
`- Assistant text is never delivered. End the run with \`${FINISH_AUTOMATION_RUN_TOOL_NAME}\`. Do not post the result with another tool.`
|
|
1874
|
+
];
|
|
1745
1875
|
const SAFETY_RULES = [
|
|
1746
1876
|
"- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
|
|
1747
1877
|
"- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
|
|
1748
1878
|
"- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
|
|
1749
1879
|
];
|
|
1880
|
+
const AUTOMATION_FAILURE_RULES = ["- Keep commands, error output, raw tool payloads, and internal routing metadata out of the message.", `- When a failure stops the job, put its cause in the \`${FINISH_AUTOMATION_RUN_TOOL_NAME}\` reason, not in the message.`];
|
|
1750
1881
|
const FAILURE_RULES = [
|
|
1751
1882
|
"- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
|
|
1752
1883
|
"- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
|
|
@@ -1759,21 +1890,33 @@ function renderRuleSection(tag, lines) {
|
|
|
1759
1890
|
`</${tag}>`
|
|
1760
1891
|
].join("\n");
|
|
1761
1892
|
}
|
|
1762
|
-
function buildBehaviorSection(platform) {
|
|
1763
|
-
const
|
|
1893
|
+
function buildBehaviorSection(platform, mode) {
|
|
1894
|
+
const runSections = mode === "automation" ? [renderRuleSection("automation-run", AUTOMATION_RUN_RULES)] : [
|
|
1895
|
+
renderRuleSection("task-execution", TASK_EXECUTION_RULES),
|
|
1896
|
+
renderRuleSection("conversation", CONVERSATION_RULES),
|
|
1897
|
+
...platform === "slack" ? [renderRuleSection("slack-actions", SLACK_ACTION_RULES)] : []
|
|
1898
|
+
];
|
|
1899
|
+
const automation = mode === "automation";
|
|
1900
|
+
return [
|
|
1764
1901
|
renderRuleSection("tool-policy", TOOL_POLICY_RULES),
|
|
1765
|
-
renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
|
|
1902
|
+
...automation ? [] : [renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES)],
|
|
1766
1903
|
renderRuleSection("skill-policy", SKILL_POLICY_RULES),
|
|
1767
1904
|
renderRuleSection("planning", PLANNING_RULES),
|
|
1768
|
-
|
|
1769
|
-
renderRuleSection("conversation", CONVERSATION_RULES),
|
|
1905
|
+
...runSections,
|
|
1770
1906
|
renderRuleSection("safety", SAFETY_RULES),
|
|
1771
|
-
renderRuleSection("failure-handling", FAILURE_RULES)
|
|
1772
|
-
];
|
|
1773
|
-
|
|
1774
|
-
|
|
1907
|
+
renderRuleSection("failure-handling", automation ? AUTOMATION_FAILURE_RULES : FAILURE_RULES)
|
|
1908
|
+
].join("\n\n");
|
|
1909
|
+
}
|
|
1910
|
+
function buildAutomationOutputSection(platform) {
|
|
1911
|
+
return [
|
|
1912
|
+
platform === "slack" ? `<output format="slack-markdown">` : `<output format="markdown">`,
|
|
1913
|
+
"- Follow any format in the instruction. Otherwise keep the message as short as the job allows, and do not describe the run.",
|
|
1914
|
+
platform === "slack" ? "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables." : "- Use concise Markdown: short paragraphs, bullets, links, and fenced code blocks.",
|
|
1915
|
+
"</output>"
|
|
1916
|
+
].join("\n");
|
|
1775
1917
|
}
|
|
1776
|
-
function buildOutputSection(platform) {
|
|
1918
|
+
function buildOutputSection(platform, mode) {
|
|
1919
|
+
if (mode === "automation") return buildAutomationOutputSection(platform);
|
|
1777
1920
|
if (platform === "local") return [
|
|
1778
1921
|
`<output format="markdown">`,
|
|
1779
1922
|
"- Start with the answer or result, not internal process narration.",
|
|
@@ -1841,13 +1984,22 @@ function formatDestinationLines(destination) {
|
|
|
1841
1984
|
`- destination.channel_id: ${escapeXml(destination.channelId)}`
|
|
1842
1985
|
];
|
|
1843
1986
|
}
|
|
1987
|
+
function formatOutcomeLines(outcomes) {
|
|
1988
|
+
if (!outcomes) return ["- dispatch.outcomes: send_message to the destination below (legacy default)"];
|
|
1989
|
+
if (outcomes.length === 0) return ["- dispatch.outcomes: none; successful work posts nothing"];
|
|
1990
|
+
return outcomes.map((outcome, index) => {
|
|
1991
|
+
const { channelId, threadTs } = outcome.destination;
|
|
1992
|
+
const recipient = isDmChannel$1(channelId) ? "the creator's direct message" : "a slack channel";
|
|
1993
|
+
const target = [`channel_id=${escapeXml(channelId)}`, ...threadTs ? [`thread_ts=${escapeXml(threadTs)}`] : []].join(" ");
|
|
1994
|
+
return `- dispatch.outcome.${index + 1}: ${outcome.action} to ${recipient} (${target})`;
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1844
1997
|
function buildDispatchSection(params) {
|
|
1845
1998
|
if (!params) return null;
|
|
1846
1999
|
const metadataLines = Object.entries(params.metadata ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `- dispatch.metadata.${escapeXml(key)}: ${escapeXml(value)}`);
|
|
1847
2000
|
return renderTag("dispatch", [
|
|
1848
2001
|
"- dispatch.execution: execute the dispatched input now",
|
|
1849
|
-
"- dispatch.delivery: the runtime delivers the final answer to the destination",
|
|
1850
|
-
"- dispatch.delivery_rule: do not request or require a separate posting tool just to deliver the final answer",
|
|
2002
|
+
...params.declaresResult ? formatOutcomeLines(params.outcomes) : ["- dispatch.delivery: the runtime delivers the final answer to the destination", "- dispatch.delivery_rule: do not request or require a separate posting tool just to deliver the final answer"],
|
|
1851
2003
|
...params.actor ? [`- dispatch.actor.platform: ${escapeXml(params.actor.platform)}`, `- dispatch.actor.name: ${escapeXml(params.actor.name)}`] : [],
|
|
1852
2004
|
...params.plugin ? [`- dispatch.plugin: ${escapeXml(params.plugin)}`] : [],
|
|
1853
2005
|
...formatSourceLines(params.source),
|
|
@@ -1897,23 +2049,29 @@ function buildPluginSystemPromptContributions(contributions) {
|
|
|
1897
2049
|
for (const contribution of contributions) lines.push(` <plugin-contribution plugin="${escapeXml(contribution.pluginName)}" id="${escapeXml(contribution.id)}">`, escapeXml(contribution.text.trim()), " </plugin-contribution>");
|
|
1898
2050
|
return renderTagBlock("plugin-system-context", lines.join("\n"));
|
|
1899
2051
|
}
|
|
1900
|
-
function buildStaticSystemPrompt(platform) {
|
|
2052
|
+
function buildStaticSystemPrompt(platform, mode) {
|
|
1901
2053
|
return [
|
|
1902
2054
|
platform === "slack" ? SLACK_HEADER : LOCAL_HEADER,
|
|
1903
2055
|
buildIdentitySection(platform),
|
|
1904
2056
|
buildPersonalitySection(),
|
|
1905
2057
|
buildWorldSection(),
|
|
1906
|
-
buildBehaviorSection(platform),
|
|
1907
|
-
buildOutputSection(platform)
|
|
2058
|
+
buildBehaviorSection(platform, mode),
|
|
2059
|
+
buildOutputSection(platform, mode)
|
|
1908
2060
|
].filter((section) => Boolean(section)).join("\n\n");
|
|
1909
2061
|
}
|
|
1910
2062
|
const STATIC_SYSTEM_PROMPTS = {
|
|
1911
|
-
|
|
1912
|
-
|
|
2063
|
+
automation: {
|
|
2064
|
+
local: buildStaticSystemPrompt("local", "automation"),
|
|
2065
|
+
slack: buildStaticSystemPrompt("slack", "automation")
|
|
2066
|
+
},
|
|
2067
|
+
conversation: {
|
|
2068
|
+
local: buildStaticSystemPrompt("local", "conversation"),
|
|
2069
|
+
slack: buildStaticSystemPrompt("slack", "conversation")
|
|
2070
|
+
}
|
|
1913
2071
|
};
|
|
1914
|
-
/** Return the fixed instructions
|
|
1915
|
-
function buildSystemPrompt(platform) {
|
|
1916
|
-
return STATIC_SYSTEM_PROMPTS[platform];
|
|
2072
|
+
/** Return the fixed instructions for one platform and prompt mode. */
|
|
2073
|
+
function buildSystemPrompt(platform, mode = "conversation") {
|
|
2074
|
+
return STATIC_SYSTEM_PROMPTS[mode][platform];
|
|
1917
2075
|
}
|
|
1918
2076
|
/** Build volatile runtime context that belongs in the user turn, not the system prompt. */
|
|
1919
2077
|
function buildTurnContextPrompt(params) {
|
|
@@ -2200,9 +2358,10 @@ async function assemblePrompt(args) {
|
|
|
2200
2358
|
userContentParts: requestContentParts
|
|
2201
2359
|
}) : void 0;
|
|
2202
2360
|
const needsBootstrapContextForPrompt = shouldPromptAgent && !replayedPrompt && !hasRuntimeTurnContext(promptHistoryMessages);
|
|
2203
|
-
const
|
|
2361
|
+
const promptMode = args.run.dispatch?.declaresResult ? "automation" : "conversation";
|
|
2362
|
+
const platform = (args.run.delivery || promptMode === "automation") && args.run.location?.provider === "slack" ? "slack" : "local";
|
|
2204
2363
|
const pluginSystemPrompt = buildPluginSystemPromptContributions(await getPluginSystemPromptContributions(platform));
|
|
2205
|
-
const baseInstructions = [buildSystemPrompt(platform), pluginSystemPrompt].filter((section) => Boolean(section)).join("\n\n");
|
|
2364
|
+
const baseInstructions = [buildSystemPrompt(platform, promptMode), pluginSystemPrompt].filter((section) => Boolean(section)).join("\n\n");
|
|
2206
2365
|
const pluginUserPromptContributions = !shouldPromptAgent || replayedPrompt ? [] : await getPluginUserPromptContributions({
|
|
2207
2366
|
context: args.toolRuntimeContext,
|
|
2208
2367
|
turnId: args.turnId
|
|
@@ -2661,541 +2820,6 @@ function formatSandboxCommandResult(params) {
|
|
|
2661
2820
|
});
|
|
2662
2821
|
}
|
|
2663
2822
|
//#endregion
|
|
2664
|
-
//#region src/chat/slack/post-warning.ts
|
|
2665
|
-
const codes = /* @__PURE__ */ new Set([
|
|
2666
|
-
"invalid_metadata_format",
|
|
2667
|
-
"invalid_metadata_schema",
|
|
2668
|
-
"metadata_too_large",
|
|
2669
|
-
"invalid_arguments",
|
|
2670
|
-
"invalid_blocks",
|
|
2671
|
-
"missing_scope",
|
|
2672
|
-
"missing_charset",
|
|
2673
|
-
"superfluous_charset",
|
|
2674
|
-
"message_truncated"
|
|
2675
|
-
]);
|
|
2676
|
-
const pathFields = /* @__PURE__ */ new Set([
|
|
2677
|
-
"metadata",
|
|
2678
|
-
"event_type",
|
|
2679
|
-
"event_payload",
|
|
2680
|
-
"entities",
|
|
2681
|
-
"entity_type",
|
|
2682
|
-
"entity_payload",
|
|
2683
|
-
"external_ref",
|
|
2684
|
-
"id",
|
|
2685
|
-
"type",
|
|
2686
|
-
"url",
|
|
2687
|
-
"attributes",
|
|
2688
|
-
"title",
|
|
2689
|
-
"text",
|
|
2690
|
-
"display_id",
|
|
2691
|
-
"display_type",
|
|
2692
|
-
"product_name",
|
|
2693
|
-
"fields",
|
|
2694
|
-
"status",
|
|
2695
|
-
"custom_fields",
|
|
2696
|
-
"key",
|
|
2697
|
-
"label",
|
|
2698
|
-
"value",
|
|
2699
|
-
"long",
|
|
2700
|
-
"format",
|
|
2701
|
-
"link",
|
|
2702
|
-
"display_order",
|
|
2703
|
-
"app_unfurl_url",
|
|
2704
|
-
"app_id",
|
|
2705
|
-
"created_by",
|
|
2706
|
-
"created_at",
|
|
2707
|
-
"updated_by",
|
|
2708
|
-
"updated_at",
|
|
2709
|
-
"full_size_preview",
|
|
2710
|
-
"preview_url",
|
|
2711
|
-
"mime_type"
|
|
2712
|
-
]);
|
|
2713
|
-
const diagnosticWords = new Set(`WARN ERROR Message message metadata was incorrectly formatted The the will be
|
|
2714
|
-
ignored as a result For for event entity refer to following errors error
|
|
2715
|
-
missing required field fields property properties additional unexpected unknown
|
|
2716
|
-
unsupported invalid expected received actual type types must should match matches
|
|
2717
|
-
matching does not is are has have of at in on and or an only allowed allow
|
|
2718
|
-
value values string number integer boolean object array null minimum maximum
|
|
2719
|
-
length min max items item empty nonempty too long short large small size limit
|
|
2720
|
-
exceeded exceeds less greater than equal valid format schema validation failed
|
|
2721
|
-
failure cannot contain contains found one any all none enum pattern definition
|
|
2722
|
-
defined constraint constraints satisfy satisfies with without provided specified
|
|
2723
|
-
unrecognized unrecognised recognized recognised extraneous disallowed duplicate
|
|
2724
|
-
unique malformed nested root exactly needs need requires requirement supported
|
|
2725
|
-
json-pointer input different schema_uri schema_id schemas additionalProperties
|
|
2726
|
-
minLength maxLength minItems maxItems anyOf oneOf allOf`.split(/\s+/));
|
|
2727
|
-
function summarizeDiagnostic(value) {
|
|
2728
|
-
return value.slice(0, 2e3).replace(/\b(?:https?:\/\/|www\.)[^\s"'`<>]+/gi, "[value]").replace(/\/[^\s"'`[\],;()]*|[\p{L}\p{N}\p{M}_]+(?:[-./:@=+%~][\p{L}\p{N}\p{M}_]+)*|[^\s\w[\]{}():,;."'`/*=<>!?+-]/gu, (token) => {
|
|
2729
|
-
if (token.startsWith("/")) return token.split("/").slice(0, 17).map((part, index) => index === 0 ? "" : pathFields.has(part) ? part : "*").join("/");
|
|
2730
|
-
return codes.has(token) || pathFields.has(token) || diagnosticWords.has(token) ? token : "[value]";
|
|
2731
|
-
}).replace(/\s+/g, " ").slice(0, 2e3);
|
|
2732
|
-
}
|
|
2733
|
-
/** Report Slack warnings once, on the request span, without retrying the post. */
|
|
2734
|
-
function captureSlackPostWarning(response, attributes) {
|
|
2735
|
-
const warnings = response.response_metadata?.warnings ?? [];
|
|
2736
|
-
const messages = response.response_metadata?.messages ?? [];
|
|
2737
|
-
const knownCodes = [...new Set(warnings.slice(0, 20).filter((code) => codes.has(code)))].sort();
|
|
2738
|
-
const diagnostics = {
|
|
2739
|
-
"app.slack.warning_count": warnings.length,
|
|
2740
|
-
"app.slack.response_message_count": messages.length,
|
|
2741
|
-
"app.slack.diagnostic_codes": knownCodes
|
|
2742
|
-
};
|
|
2743
|
-
setSpanAttributes(diagnostics);
|
|
2744
|
-
if (!warnings.length && !messages.length) return;
|
|
2745
|
-
captureMessage("Slack chat.postMessage returned warnings", {
|
|
2746
|
-
level: "warning",
|
|
2747
|
-
fingerprint: ["slack.chat.postMessage.warning", ...knownCodes.length ? knownCodes : ["unknown"]],
|
|
2748
|
-
tags: { "app.slack.method": "chat.postMessage" },
|
|
2749
|
-
extra: {
|
|
2750
|
-
...getLogContextAttributes(),
|
|
2751
|
-
...attributes,
|
|
2752
|
-
...diagnostics,
|
|
2753
|
-
"app.slack.response_diagnostics": {
|
|
2754
|
-
warnings: warnings.slice(0, 20).map(summarizeDiagnostic),
|
|
2755
|
-
messages: messages.slice(0, 20).map(summarizeDiagnostic)
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
});
|
|
2759
|
-
}
|
|
2760
|
-
//#endregion
|
|
2761
|
-
//#region src/chat/slack/work-object.ts
|
|
2762
|
-
const url = z.url({ protocol: /^https?$/ });
|
|
2763
|
-
const textField = z.strictObject({
|
|
2764
|
-
type: z.literal("string"),
|
|
2765
|
-
key: z.string().min(1),
|
|
2766
|
-
label: z.string().min(1),
|
|
2767
|
-
value: z.string(),
|
|
2768
|
-
long: z.boolean().optional(),
|
|
2769
|
-
format: z.literal("markdown").optional(),
|
|
2770
|
-
link: url.optional()
|
|
2771
|
-
}).refine((field) => !field.format || !field.link, { message: "Slack string fields cannot combine format and link" });
|
|
2772
|
-
const customField = z.discriminatedUnion("type", [textField, z.strictObject({
|
|
2773
|
-
type: z.literal("slack#/types/timestamp"),
|
|
2774
|
-
key: z.string().min(1),
|
|
2775
|
-
label: z.string().min(1),
|
|
2776
|
-
value: z.number().int(),
|
|
2777
|
-
link: url.optional()
|
|
2778
|
-
})]);
|
|
2779
|
-
const payload = z.strictObject({
|
|
2780
|
-
attributes: z.strictObject({
|
|
2781
|
-
title: z.strictObject({ text: z.string().min(1) }),
|
|
2782
|
-
display_id: z.string().optional(),
|
|
2783
|
-
display_type: z.string().optional(),
|
|
2784
|
-
product_name: z.string().optional(),
|
|
2785
|
-
product_icon: z.strictObject({
|
|
2786
|
-
url,
|
|
2787
|
-
alt_text: z.string().min(1)
|
|
2788
|
-
}).optional()
|
|
2789
|
-
}),
|
|
2790
|
-
custom_fields: z.array(customField).optional(),
|
|
2791
|
-
display_order: z.array(z.string()).optional()
|
|
2792
|
-
});
|
|
2793
|
-
/** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
|
|
2794
|
-
const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
|
|
2795
|
-
const entity = z.strictObject({
|
|
2796
|
-
external_ref: z.strictObject({
|
|
2797
|
-
id: slackExternalRefIdSchema,
|
|
2798
|
-
type: z.string().min(1).optional()
|
|
2799
|
-
}),
|
|
2800
|
-
url
|
|
2801
|
-
});
|
|
2802
|
-
/** Validate notification and detail metadata before Slack can ignore bad fields. */
|
|
2803
|
-
const slackEntitySchema = z.discriminatedUnion("entity_type", [entity.extend({
|
|
2804
|
-
entity_type: z.literal("slack#/entities/item"),
|
|
2805
|
-
entity_payload: payload.extend({ fields: z.never().optional() })
|
|
2806
|
-
}), entity.extend({
|
|
2807
|
-
entity_type: z.literal("slack#/entities/task"),
|
|
2808
|
-
entity_payload: payload.extend({ fields: z.strictObject({ status: z.strictObject({ value: z.string() }).optional() }).optional() })
|
|
2809
|
-
})]);
|
|
2810
|
-
//#endregion
|
|
2811
|
-
//#region src/chat/slack/outbound.ts
|
|
2812
|
-
const MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
|
|
2813
|
-
function requireSlackConversationId(channelId, action) {
|
|
2814
|
-
const normalized = normalizeSlackConversationId(channelId);
|
|
2815
|
-
if (!normalized) throw new Error(`${action} requires a valid channel ID`);
|
|
2816
|
-
return normalized;
|
|
2817
|
-
}
|
|
2818
|
-
function requireSlackThreadTimestamp(threadTs, action) {
|
|
2819
|
-
const normalized = threadTs.trim();
|
|
2820
|
-
if (!normalized) throw new Error(`${action} requires a thread timestamp`);
|
|
2821
|
-
return normalized;
|
|
2822
|
-
}
|
|
2823
|
-
function requireSlackMessageTimestamp(timestamp, action) {
|
|
2824
|
-
const normalized = parseSlackMessageTs(timestamp);
|
|
2825
|
-
if (!normalized) throw new Error(`${action} requires a target message timestamp`);
|
|
2826
|
-
return normalized;
|
|
2827
|
-
}
|
|
2828
|
-
function requireSlackMessageText(text, action) {
|
|
2829
|
-
if (text.trim().length === 0) throw new Error(`${action} requires non-empty text`);
|
|
2830
|
-
if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) throw new Error(`${action} text exceeds Slack's 40000 character truncation limit`);
|
|
2831
|
-
return text;
|
|
2832
|
-
}
|
|
2833
|
-
/** Resolve a Slack message permalink without making read-only workflows fail on lookup errors. */
|
|
2834
|
-
async function getSlackMessagePermalink(args) {
|
|
2835
|
-
try {
|
|
2836
|
-
return (await withSlackRetries(() => getSlackClient().chat.getPermalink({
|
|
2837
|
-
channel: args.channelId,
|
|
2838
|
-
message_ts: args.messageTs
|
|
2839
|
-
}), 3, {
|
|
2840
|
-
action: "chat.getPermalink",
|
|
2841
|
-
idempotent: true,
|
|
2842
|
-
spanAttributes: {
|
|
2843
|
-
"app.slack.channel_id": args.channelId,
|
|
2844
|
-
"app.slack.message_ts": args.messageTs
|
|
2845
|
-
}
|
|
2846
|
-
})).permalink;
|
|
2847
|
-
} catch {
|
|
2848
|
-
return;
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
/** Post Slack `mrkdwn` text to a conversation or thread via the shared outbound boundary. */
|
|
2852
|
-
async function postSlackMessage(input) {
|
|
2853
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack message posting");
|
|
2854
|
-
const text = requireSlackMessageText(input.text, "Slack message posting");
|
|
2855
|
-
const entities = input.entities?.length ? slackEntitySchema.array().parse(input.entities) : void 0;
|
|
2856
|
-
const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack thread message posting") : void 0;
|
|
2857
|
-
const attributes = {
|
|
2858
|
-
"app.slack.channel_id": channelId,
|
|
2859
|
-
...threadTs ? { "app.slack.thread_ts": threadTs } : void 0,
|
|
2860
|
-
"app.slack.work_object.count": entities?.length ?? 0
|
|
2861
|
-
};
|
|
2862
|
-
const response = await withSlackRetries(async () => {
|
|
2863
|
-
const response = await getSlackClient().chat.postMessage({
|
|
2864
|
-
channel: channelId,
|
|
2865
|
-
text,
|
|
2866
|
-
unfurl_links: false,
|
|
2867
|
-
unfurl_media: false,
|
|
2868
|
-
...input.blocks?.length ? { blocks: input.blocks } : void 0,
|
|
2869
|
-
...entities ? { metadata: { entities } } : void 0,
|
|
2870
|
-
...threadTs ? { thread_ts: threadTs } : void 0
|
|
2871
|
-
});
|
|
2872
|
-
const messageId = parseSlackMessageTs(response.ts);
|
|
2873
|
-
setSpanAttributes({ "messaging.message.id": messageId });
|
|
2874
|
-
captureSlackPostWarning(response, {
|
|
2875
|
-
...attributes,
|
|
2876
|
-
"messaging.message.id": messageId
|
|
2877
|
-
});
|
|
2878
|
-
return response;
|
|
2879
|
-
}, 3, {
|
|
2880
|
-
action: "chat.postMessage",
|
|
2881
|
-
attributes
|
|
2882
|
-
});
|
|
2883
|
-
const messageTs = parseSlackMessageTs(response.ts);
|
|
2884
|
-
if (!messageTs) throw new Error("Slack message posted without ts");
|
|
2885
|
-
return {
|
|
2886
|
-
ts: messageTs,
|
|
2887
|
-
...input.includePermalink ? { permalink: await getSlackMessagePermalink({
|
|
2888
|
-
channelId,
|
|
2889
|
-
messageTs
|
|
2890
|
-
}) } : void 0
|
|
2891
|
-
};
|
|
2892
|
-
}
|
|
2893
|
-
/**
|
|
2894
|
-
* Post an ephemeral Slack message. Delivery is best-effort on Slack's side, but
|
|
2895
|
-
* request validation and Web API behavior are centralized here.
|
|
2896
|
-
*/
|
|
2897
|
-
async function postSlackEphemeralMessage(input) {
|
|
2898
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack ephemeral message posting");
|
|
2899
|
-
const userId = parseSlackUserId(input.userId);
|
|
2900
|
-
if (!userId) throw new Error("Slack ephemeral message posting requires a user ID");
|
|
2901
|
-
const text = requireSlackMessageText(input.text, "Slack ephemeral message posting");
|
|
2902
|
-
const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack ephemeral thread message posting") : void 0;
|
|
2903
|
-
return { messageTs: (await withSlackRetries(() => getSlackClient().chat.postEphemeral({
|
|
2904
|
-
channel: channelId,
|
|
2905
|
-
user: userId,
|
|
2906
|
-
text,
|
|
2907
|
-
...input.blocks?.length ? { blocks: input.blocks } : void 0,
|
|
2908
|
-
...threadTs ? { thread_ts: threadTs } : void 0
|
|
2909
|
-
}), 3, {
|
|
2910
|
-
action: "chat.postEphemeral",
|
|
2911
|
-
spanAttributes: {
|
|
2912
|
-
"app.slack.channel_id": channelId,
|
|
2913
|
-
"app.slack.user_id": userId,
|
|
2914
|
-
...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
|
|
2915
|
-
}
|
|
2916
|
-
})).message_ts };
|
|
2917
|
-
}
|
|
2918
|
-
/** Upload files into a Slack conversation or thread via the shared outbound file boundary. */
|
|
2919
|
-
async function uploadFilesToConversation(input) {
|
|
2920
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack file upload");
|
|
2921
|
-
const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
|
|
2922
|
-
if (input.files.length === 0) throw new Error("Slack file upload requires at least one file");
|
|
2923
|
-
const fileUploads = input.files.map((file) => {
|
|
2924
|
-
const filename = file.filename.trim();
|
|
2925
|
-
if (!filename) throw new Error("Slack file upload requires every file to have a filename");
|
|
2926
|
-
return {
|
|
2927
|
-
file: file.data,
|
|
2928
|
-
filename
|
|
2929
|
-
};
|
|
2930
|
-
});
|
|
2931
|
-
return { files: ((await withSlackRetries(() => getSlackClient().filesUploadV2({
|
|
2932
|
-
channel_id: channelId,
|
|
2933
|
-
...threadTs ? { thread_ts: threadTs } : void 0,
|
|
2934
|
-
file_uploads: fileUploads
|
|
2935
|
-
}), 3, {
|
|
2936
|
-
action: "filesUploadV2",
|
|
2937
|
-
spanAttributes: {
|
|
2938
|
-
"app.slack.channel_id": channelId,
|
|
2939
|
-
...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
|
|
2940
|
-
}
|
|
2941
|
-
})).files ?? []).flatMap((completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})) };
|
|
2942
|
-
}
|
|
2943
|
-
/** Add a reaction to a Slack message, treating `already_reacted` as idempotent success. */
|
|
2944
|
-
async function addReactionToMessage(input) {
|
|
2945
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack reaction");
|
|
2946
|
-
const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction");
|
|
2947
|
-
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
2948
|
-
if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
|
|
2949
|
-
try {
|
|
2950
|
-
await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
|
|
2951
|
-
channel: channelId,
|
|
2952
|
-
timestamp,
|
|
2953
|
-
name: emoji
|
|
2954
|
-
}), input.timeoutMs === void 0 ? 3 : 1, {
|
|
2955
|
-
action: "reactions.add",
|
|
2956
|
-
idempotent: true,
|
|
2957
|
-
spanAttributes: {
|
|
2958
|
-
"app.slack.channel_id": channelId,
|
|
2959
|
-
"app.slack.message_ts": timestamp,
|
|
2960
|
-
"app.slack.reaction": emoji
|
|
2961
|
-
}
|
|
2962
|
-
});
|
|
2963
|
-
} catch (error) {
|
|
2964
|
-
if (error instanceof SlackActionError && error.code === "already_reacted") return { ok: true };
|
|
2965
|
-
throw error;
|
|
2966
|
-
}
|
|
2967
|
-
return { ok: true };
|
|
2968
|
-
}
|
|
2969
|
-
/** Remove a reaction from a Slack message, treating `no_reaction` as idempotent success. */
|
|
2970
|
-
async function removeReactionFromMessage(input) {
|
|
2971
|
-
const channelId = requireSlackConversationId(input.channelId, "Slack reaction removal");
|
|
2972
|
-
const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction removal");
|
|
2973
|
-
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
2974
|
-
if (!emoji) throw new Error("Slack reaction removal requires a valid emoji alias name");
|
|
2975
|
-
try {
|
|
2976
|
-
await withSlackRetries(() => getSlackClient().reactions.remove({
|
|
2977
|
-
channel: channelId,
|
|
2978
|
-
timestamp,
|
|
2979
|
-
name: emoji
|
|
2980
|
-
}), 3, {
|
|
2981
|
-
action: "reactions.remove",
|
|
2982
|
-
idempotent: true,
|
|
2983
|
-
spanAttributes: {
|
|
2984
|
-
"app.slack.channel_id": channelId,
|
|
2985
|
-
"app.slack.message_ts": timestamp,
|
|
2986
|
-
"app.slack.reaction": emoji
|
|
2987
|
-
}
|
|
2988
|
-
});
|
|
2989
|
-
} catch (error) {
|
|
2990
|
-
if (error instanceof SlackActionError && error.code === "no_reaction") return { ok: true };
|
|
2991
|
-
throw error;
|
|
2992
|
-
}
|
|
2993
|
-
return { ok: true };
|
|
2994
|
-
}
|
|
2995
|
-
//#endregion
|
|
2996
|
-
//#region src/chat/slack/status-format.ts
|
|
2997
|
-
const SLACK_STATUS_MAX_LENGTH = 50;
|
|
2998
|
-
/** Truncate a status string to fit Slack's 50-char assistant status limit. */
|
|
2999
|
-
function truncateStatusText(text) {
|
|
3000
|
-
const trimmed = text.trim();
|
|
3001
|
-
if (!trimmed) return "";
|
|
3002
|
-
if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) return trimmed;
|
|
3003
|
-
return `${trimmed.slice(0, 47).trimEnd()}...`;
|
|
3004
|
-
}
|
|
3005
|
-
//#endregion
|
|
3006
|
-
//#region src/chat/slack/mrkdwn.ts
|
|
3007
|
-
/** Escape dynamic text for Slack mrkdwn without changing intended formatting. */
|
|
3008
|
-
function escapeSlackMrkdwnText(text) {
|
|
3009
|
-
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
3010
|
-
}
|
|
3011
|
-
/** Escape a URL for Slack explicit link syntax while preserving query semantics. */
|
|
3012
|
-
function escapeSlackLinkUrl(url) {
|
|
3013
|
-
return url.replaceAll("&", "&").replaceAll("<", "%3C").replaceAll(">", "%3E");
|
|
3014
|
-
}
|
|
3015
|
-
/** Build a Slack explicit link from dynamic URL and label values. */
|
|
3016
|
-
function formatSlackLink(url, label) {
|
|
3017
|
-
return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
|
|
3018
|
-
}
|
|
3019
|
-
function readInlineCodeSpan(line, start) {
|
|
3020
|
-
if (line[start] !== "`") return;
|
|
3021
|
-
let n = 1;
|
|
3022
|
-
while (line[start + n] === "`") n++;
|
|
3023
|
-
const marker = "`".repeat(n);
|
|
3024
|
-
let search = start + n;
|
|
3025
|
-
while (search < line.length) {
|
|
3026
|
-
const close = line.indexOf(marker, search);
|
|
3027
|
-
if (close === -1) return;
|
|
3028
|
-
const after = close + n;
|
|
3029
|
-
if (line[after] !== "`") return {
|
|
3030
|
-
text: line.slice(start, after),
|
|
3031
|
-
end: after
|
|
3032
|
-
};
|
|
3033
|
-
search = after + 1;
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
function readExistingSlackAngleToken(line, start) {
|
|
3037
|
-
if (line[start] !== "<") return;
|
|
3038
|
-
const close = line.indexOf(">", start + 1);
|
|
3039
|
-
if (close === -1) return;
|
|
3040
|
-
const body = line.slice(start + 1, close);
|
|
3041
|
-
if (/^(?:https?:\/\/|@|#|!)/.test(body)) return {
|
|
3042
|
-
text: line.slice(start, close + 1),
|
|
3043
|
-
end: close + 1
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
function readMarkdownLink(line, start) {
|
|
3047
|
-
if (line[start] !== "[") return;
|
|
3048
|
-
const labelEnd = line.indexOf("](", start + 1);
|
|
3049
|
-
if (labelEnd === -1) return;
|
|
3050
|
-
const destStart = labelEnd + 2;
|
|
3051
|
-
if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) return;
|
|
3052
|
-
const closeParens = line.indexOf(")", destStart);
|
|
3053
|
-
if (closeParens === -1) return;
|
|
3054
|
-
return {
|
|
3055
|
-
text: line.slice(start, closeParens + 1),
|
|
3056
|
-
end: closeParens + 1
|
|
3057
|
-
};
|
|
3058
|
-
}
|
|
3059
|
-
function hasUnmatchedClosingParen(text) {
|
|
3060
|
-
let balance = 0;
|
|
3061
|
-
for (const ch of text) if (ch === "(") balance++;
|
|
3062
|
-
else if (ch === ")") balance--;
|
|
3063
|
-
return balance < 0;
|
|
3064
|
-
}
|
|
3065
|
-
function readBareUrl(line, start) {
|
|
3066
|
-
let end = start;
|
|
3067
|
-
while (end < line.length) {
|
|
3068
|
-
const ch = line[end];
|
|
3069
|
-
if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === "\"" || ch === "`" || ch === "|" || ch === "*") break;
|
|
3070
|
-
end++;
|
|
3071
|
-
}
|
|
3072
|
-
if (end === start) return;
|
|
3073
|
-
let raw = line.slice(start, end);
|
|
3074
|
-
let suffix = "";
|
|
3075
|
-
const peel = () => {
|
|
3076
|
-
suffix = raw.slice(-1) + suffix;
|
|
3077
|
-
raw = raw.slice(0, -1);
|
|
3078
|
-
};
|
|
3079
|
-
const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
|
|
3080
|
-
while (raw.length > 0 && shouldPeel()) peel();
|
|
3081
|
-
if (!/^https?:\/\/.+/.test(raw)) return;
|
|
3082
|
-
return {
|
|
3083
|
-
url: raw,
|
|
3084
|
-
suffix,
|
|
3085
|
-
end
|
|
3086
|
-
};
|
|
3087
|
-
}
|
|
3088
|
-
/** Wrap bare http(s) URLs on a single line as Slack explicit `<url>` links. */
|
|
3089
|
-
function wrapBareUrlsOnLine(line) {
|
|
3090
|
-
let result = "";
|
|
3091
|
-
let i = 0;
|
|
3092
|
-
while (i < line.length) {
|
|
3093
|
-
const codeSpan = readInlineCodeSpan(line, i);
|
|
3094
|
-
if (codeSpan) {
|
|
3095
|
-
result += codeSpan.text;
|
|
3096
|
-
i = codeSpan.end;
|
|
3097
|
-
continue;
|
|
3098
|
-
}
|
|
3099
|
-
const angleToken = readExistingSlackAngleToken(line, i);
|
|
3100
|
-
if (angleToken) {
|
|
3101
|
-
result += angleToken.text;
|
|
3102
|
-
i = angleToken.end;
|
|
3103
|
-
continue;
|
|
3104
|
-
}
|
|
3105
|
-
const mdLink = readMarkdownLink(line, i);
|
|
3106
|
-
if (mdLink) {
|
|
3107
|
-
result += mdLink.text;
|
|
3108
|
-
i = mdLink.end;
|
|
3109
|
-
continue;
|
|
3110
|
-
}
|
|
3111
|
-
if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
|
|
3112
|
-
const parsed = readBareUrl(line, i);
|
|
3113
|
-
if (parsed) {
|
|
3114
|
-
result += `<${parsed.url}>${parsed.suffix}`;
|
|
3115
|
-
i = parsed.end;
|
|
3116
|
-
continue;
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
result += line[i];
|
|
3120
|
-
i++;
|
|
3121
|
-
}
|
|
3122
|
-
return result;
|
|
3123
|
-
}
|
|
3124
|
-
/**
|
|
3125
|
-
* Pre-wrap bare http(s) URLs outside fenced code blocks as Slack explicit
|
|
3126
|
-
* links, preventing Slack's auto-linker from consuming adjacent formatting
|
|
3127
|
-
* markers into the URL.
|
|
3128
|
-
*
|
|
3129
|
-
* Uses the same fence-toggle rule as `ensureBlockSpacing` so both passes
|
|
3130
|
-
* agree on which lines are code.
|
|
3131
|
-
*/
|
|
3132
|
-
function wrapBareUrls(text) {
|
|
3133
|
-
const lines = text.split("\n");
|
|
3134
|
-
const out = [];
|
|
3135
|
-
let inCodeBlock = false;
|
|
3136
|
-
for (const line of lines) {
|
|
3137
|
-
if (line.trimStart().startsWith("```")) {
|
|
3138
|
-
inCodeBlock = !inCodeBlock;
|
|
3139
|
-
out.push(line);
|
|
3140
|
-
continue;
|
|
3141
|
-
}
|
|
3142
|
-
out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
|
|
3143
|
-
}
|
|
3144
|
-
return out.join("\n");
|
|
3145
|
-
}
|
|
3146
|
-
/** Insert blank lines between content blocks so Slack renders them with visual separation. */
|
|
3147
|
-
function ensureBlockSpacing(text) {
|
|
3148
|
-
const codeBlockPattern = /^```/;
|
|
3149
|
-
const listItemPattern = /^[-*•]\s|^\d+\.\s/;
|
|
3150
|
-
const lines = text.split("\n");
|
|
3151
|
-
const result = [];
|
|
3152
|
-
let inCodeBlock = false;
|
|
3153
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3154
|
-
const line = lines[i];
|
|
3155
|
-
if (codeBlockPattern.test(line.trimStart())) {
|
|
3156
|
-
if (!inCodeBlock) {
|
|
3157
|
-
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
3158
|
-
if (prev !== void 0 && prev.trim() !== "") result.push("");
|
|
3159
|
-
}
|
|
3160
|
-
inCodeBlock = !inCodeBlock;
|
|
3161
|
-
result.push(line);
|
|
3162
|
-
continue;
|
|
3163
|
-
}
|
|
3164
|
-
if (inCodeBlock) {
|
|
3165
|
-
result.push(line);
|
|
3166
|
-
continue;
|
|
3167
|
-
}
|
|
3168
|
-
const prev = result.length > 0 ? result[result.length - 1] : void 0;
|
|
3169
|
-
if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) result.push("");
|
|
3170
|
-
result.push(line);
|
|
3171
|
-
}
|
|
3172
|
-
return result.join("\n");
|
|
3173
|
-
}
|
|
3174
|
-
/**
|
|
3175
|
-
* Normalize model-authored Slack markdown for delivery via `markdown_text`
|
|
3176
|
-
* or `{ type: "markdown" }` blocks.
|
|
3177
|
-
*
|
|
3178
|
-
* Plugins may first rewrite ordinary Markdown. Core then applies Slack-only
|
|
3179
|
-
* delivery shaping: bare URL wrapping and block spacing. Slack reply delivery
|
|
3180
|
-
* owns chunking and continuation markers separately.
|
|
3181
|
-
*/
|
|
3182
|
-
function normalizeSlackReplyMarkdown(text) {
|
|
3183
|
-
let normalized = applyPluginFormatMarkdown(text).replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
|
|
3184
|
-
normalized = wrapBareUrls(normalized);
|
|
3185
|
-
normalized = ensureBlockSpacing(normalized);
|
|
3186
|
-
return normalized.replace(/\n{3,}/g, "\n\n").trim();
|
|
3187
|
-
}
|
|
3188
|
-
/** Normalize assistant status text before handing it to Slack. */
|
|
3189
|
-
function normalizeSlackStatusText(text) {
|
|
3190
|
-
const trimmed = text.trim();
|
|
3191
|
-
if (!trimmed) return "";
|
|
3192
|
-
return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
|
|
3193
|
-
}
|
|
3194
|
-
/** Render a Slack user mention, which Slack shows as the person's name. */
|
|
3195
|
-
function slackMention(userId) {
|
|
3196
|
-
return `<@${userId}>`;
|
|
3197
|
-
}
|
|
3198
|
-
//#endregion
|
|
3199
2823
|
//#region src/chat/slack/oauth-authorization-message.ts
|
|
3200
2824
|
const ACCOUNT_SCOPE_NOTE = "This connects only your account. Junior sees only what you can see.";
|
|
3201
2825
|
/** Present private OAuth authorization with a clear action and text fallback. */
|
|
@@ -4064,10 +3688,13 @@ async function maybePostAutomatedTurnLimitNotice(args) {
|
|
|
4064
3688
|
}
|
|
4065
3689
|
//#endregion
|
|
4066
3690
|
//#region src/chat/task-input.ts
|
|
4067
|
-
/**
|
|
3691
|
+
/**
|
|
3692
|
+
* Shared closing lines. Automations pass outcomes and end with a declared
|
|
3693
|
+
* result. Watches pass no outcomes and reply in the Conversation.
|
|
3694
|
+
*/
|
|
4068
3695
|
function replyContractLines(outcomes) {
|
|
4069
|
-
if (outcomes
|
|
4070
|
-
return [
|
|
3696
|
+
if (!outcomes) return ["When you reply, follow any reply format in the instructions.", "Briefly report what you did or what is needed next."];
|
|
3697
|
+
return [`End with \`${FINISH_AUTOMATION_RUN_TOOL_NAME}\`.`];
|
|
4071
3698
|
}
|
|
4072
3699
|
function oneLine(value) {
|
|
4073
3700
|
return value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -4092,7 +3719,7 @@ function renderTaskInput(args) {
|
|
|
4092
3719
|
"This is a task, not a message from a person.",
|
|
4093
3720
|
"",
|
|
4094
3721
|
...about ? [`About: ${oneLine(about)}`] : [],
|
|
4095
|
-
...creator ? [`Created by: ${oneLine(creator)}.
|
|
3722
|
+
...creator ? [`Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person.`] : [],
|
|
4096
3723
|
`Instructions: ${instructions}`
|
|
4097
3724
|
];
|
|
4098
3725
|
const guidance = args.guidance?.trim();
|
|
@@ -10089,6 +9716,44 @@ function createSpawnAgentTool(spawnAgent) {
|
|
|
10089
9716
|
});
|
|
10090
9717
|
}
|
|
10091
9718
|
//#endregion
|
|
9719
|
+
//#region src/chat/tools/runtime/finish-automation-run.ts
|
|
9720
|
+
const RESULT_GUIDANCE = ["Use `no_action` when the run worked and nothing should be posted, such as when a condition in the instruction is not met, or when a temporary failure stopped the work.", "Use `misconfigured` when the run cannot do its job until its creator changes the automation: a provider, tool, account, or access that the job needs is missing, a target no longer exists, or the instruction cannot be done. Do not use it for a temporary failure or a condition that is not met."].join(" ");
|
|
9721
|
+
/** Create the tool that ends one Automation run with a declared result. */
|
|
9722
|
+
function createFinishAutomationRunTool(options) {
|
|
9723
|
+
const results = options.sendsMessage ? [
|
|
9724
|
+
"send_message",
|
|
9725
|
+
"no_action",
|
|
9726
|
+
"misconfigured"
|
|
9727
|
+
] : ["no_action", "misconfigured"];
|
|
9728
|
+
return zodTool({
|
|
9729
|
+
annotations: {
|
|
9730
|
+
destructiveHint: false,
|
|
9731
|
+
idempotentHint: true,
|
|
9732
|
+
openWorldHint: false,
|
|
9733
|
+
readOnlyHint: false
|
|
9734
|
+
},
|
|
9735
|
+
description: options.sendsMessage ? `End this automation run. Use \`send_message\` with the finished reminder, digest, alert, or answer; the runtime posts it to the stored destinations. ${RESULT_GUIDANCE}` : `End this automation run. This automation posts nothing. ${RESULT_GUIDANCE}`,
|
|
9736
|
+
inputSchema: z.object({
|
|
9737
|
+
result: z.enum(results),
|
|
9738
|
+
...options.sendsMessage ? { message: z.string().optional().describe("Required for send_message. The text to post.") } : void 0,
|
|
9739
|
+
reason: z.string().optional().describe("Required for no_action and misconfigured. One sentence for the creator.")
|
|
9740
|
+
}),
|
|
9741
|
+
outputSchema: juniorToolOutputSchema.extend({ result: z.enum([
|
|
9742
|
+
"send_message",
|
|
9743
|
+
"no_action",
|
|
9744
|
+
"misconfigured"
|
|
9745
|
+
]) }),
|
|
9746
|
+
execute: async (input) => {
|
|
9747
|
+
const parsed = automationResultSchema.safeParse(input);
|
|
9748
|
+
if (!parsed.success) throw new ToolInputError(input.result === "send_message" ? "send_message requires a non-empty message." : `${input.result} requires a non-empty reason.`);
|
|
9749
|
+
return parsed.data.result === "send_message" && isNoReplyMarker(parsed.data.message) ? {
|
|
9750
|
+
result: "no_action",
|
|
9751
|
+
reason: "The message was the no-reply marker."
|
|
9752
|
+
} : parsed.data;
|
|
9753
|
+
}
|
|
9754
|
+
});
|
|
9755
|
+
}
|
|
9756
|
+
//#endregion
|
|
10092
9757
|
//#region src/chat/tools/list-watches.ts
|
|
10093
9758
|
const listedResourceWatchSchema = z.object({
|
|
10094
9759
|
id: z.string().min(1),
|
|
@@ -10433,305 +10098,6 @@ function requireRequesterMention(instruction, actor) {
|
|
|
10433
10098
|
if (namesPerson(instruction, actor.fullName)) throw new ToolInputError(`The instruction names ${actor.fullName} without a Slack mention. Write ${mention} for this person.`);
|
|
10434
10099
|
}
|
|
10435
10100
|
//#endregion
|
|
10436
|
-
//#region src/chat/automations/revision.ts
|
|
10437
|
-
/** Storage revisions prevent stale edits from replacing newer values. */
|
|
10438
|
-
var AutomationConflictError = class extends ToolInputError {
|
|
10439
|
-
constructor() {
|
|
10440
|
-
super("This Automation changed after it was loaded. Reload it before saving.");
|
|
10441
|
-
this.name = "AutomationConflictError";
|
|
10442
|
-
}
|
|
10443
|
-
};
|
|
10444
|
-
/** Sort object keys so equal values produce equal JSON. */
|
|
10445
|
-
function canonicalJson$1(value) {
|
|
10446
|
-
if (Array.isArray(value)) return value.map(canonicalJson$1);
|
|
10447
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, canonicalJson$1(child)]));
|
|
10448
|
-
return value;
|
|
10449
|
-
}
|
|
10450
|
-
/** Identify the stored values being edited, including lifecycle and creator authority. */
|
|
10451
|
-
function automationRevision(task) {
|
|
10452
|
-
return createHash("sha256").update(JSON.stringify(canonicalJson$1(task))).digest("hex");
|
|
10453
|
-
}
|
|
10454
|
-
/** Reject a stale edit while the storage owner holds the write lock. */
|
|
10455
|
-
function requireAutomationRevision(task, revision) {
|
|
10456
|
-
if (!task || automationRevision(task) !== revision) throw new AutomationConflictError();
|
|
10457
|
-
}
|
|
10458
|
-
//#endregion
|
|
10459
|
-
//#region src/chat/automations/versions.ts
|
|
10460
|
-
/** Automation versions keep each saved definition so people can see how it changed. */
|
|
10461
|
-
/** Return the versioned part of an Automation. Lifecycle and run state are excluded. */
|
|
10462
|
-
function automationDefinition(task) {
|
|
10463
|
-
const common = {
|
|
10464
|
-
title: task.title?.trim() || null,
|
|
10465
|
-
instruction: task.task.text,
|
|
10466
|
-
credentialMode: task.credentialMode,
|
|
10467
|
-
destination: task.destination,
|
|
10468
|
-
outcomes: task.outcomes
|
|
10469
|
-
};
|
|
10470
|
-
return "schedule" in task ? {
|
|
10471
|
-
...common,
|
|
10472
|
-
schedule: task.schedule
|
|
10473
|
-
} : {
|
|
10474
|
-
...common,
|
|
10475
|
-
trigger: task.trigger
|
|
10476
|
-
};
|
|
10477
|
-
}
|
|
10478
|
-
/** Compare definition values after key sorting. */
|
|
10479
|
-
function sameDefinitionValue(left, right) {
|
|
10480
|
-
return JSON.stringify(canonicalJson$1(left)) === JSON.stringify(canonicalJson$1(right));
|
|
10481
|
-
}
|
|
10482
|
-
/**
|
|
10483
|
-
* Save a version when the definition changed. Call this in the same
|
|
10484
|
-
* transaction and lock as the Automation write. Without `current`, the
|
|
10485
|
-
* Automation is new, so the creator saves version 1.
|
|
10486
|
-
*/
|
|
10487
|
-
async function recordAutomationVersion(db, kind, task, current, editedBy) {
|
|
10488
|
-
const definition = automationDefinition(task);
|
|
10489
|
-
if (current && sameDefinitionValue(automationDefinition(current), definition)) return;
|
|
10490
|
-
await db.insert(juniorAutomationVersions).values({
|
|
10491
|
-
kind,
|
|
10492
|
-
automationId: task.id,
|
|
10493
|
-
version: sql`(
|
|
10494
|
-
SELECT coalesce(max(${juniorAutomationVersions.version}), 0) + 1
|
|
10495
|
-
FROM ${juniorAutomationVersions}
|
|
10496
|
-
WHERE ${juniorAutomationVersions.kind} = ${kind}
|
|
10497
|
-
AND ${juniorAutomationVersions.automationId} = ${task.id}
|
|
10498
|
-
)`,
|
|
10499
|
-
createdAtMs: Date.now(),
|
|
10500
|
-
editedBy: editedBy ?? (current ? null : task.createdBy),
|
|
10501
|
-
definition
|
|
10502
|
-
});
|
|
10503
|
-
}
|
|
10504
|
-
function parseVersionRow(kind, row) {
|
|
10505
|
-
return automationVersionSchema.parse({
|
|
10506
|
-
kind,
|
|
10507
|
-
version: row.version,
|
|
10508
|
-
createdAt: new Date(row.createdAtMs).toISOString(),
|
|
10509
|
-
editedBy: row.editedBy,
|
|
10510
|
-
definition: row.definition
|
|
10511
|
-
});
|
|
10512
|
-
}
|
|
10513
|
-
/** Read one saved definition. */
|
|
10514
|
-
async function readAutomationVersion(db, kind, automationId, version) {
|
|
10515
|
-
const [row] = await db.select().from(juniorAutomationVersions).where(and(eq(juniorAutomationVersions.kind, kind), eq(juniorAutomationVersions.automationId, automationId), eq(juniorAutomationVersions.version, version))).limit(1);
|
|
10516
|
-
return row ? parseVersionRow(kind, row) : void 0;
|
|
10517
|
-
}
|
|
10518
|
-
/** Read saved definitions, newest first. */
|
|
10519
|
-
async function listAutomationVersions(db, kind, automationId, limit) {
|
|
10520
|
-
return (await db.select().from(juniorAutomationVersions).where(and(eq(juniorAutomationVersions.kind, kind), eq(juniorAutomationVersions.automationId, automationId))).orderBy(desc(juniorAutomationVersions.version)).limit(limit)).map((row) => parseVersionRow(kind, row));
|
|
10521
|
-
}
|
|
10522
|
-
//#endregion
|
|
10523
|
-
//#region src/chat/event-automations/store.ts
|
|
10524
|
-
const retainedEventAutomationSchema = eventAutomationSchema.extend({
|
|
10525
|
-
destination: slackDestinationSchema,
|
|
10526
|
-
outcomes: z.array(taskOutcomeSchema).max(5)
|
|
10527
|
-
});
|
|
10528
|
-
/** JSON task payload must not carry SQL-backed columns. */
|
|
10529
|
-
function eventAutomationJsonPayload(task) {
|
|
10530
|
-
const { status: _status, title: _title, ...payload } = task;
|
|
10531
|
-
return payload;
|
|
10532
|
-
}
|
|
10533
|
-
/** Decode a retained event Automation with its SQL-backed fields. */
|
|
10534
|
-
function parseEventAutomationRow(row) {
|
|
10535
|
-
const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
|
|
10536
|
-
if (raw && typeof raw === "object" && "title" in raw) delete raw.title;
|
|
10537
|
-
if (raw && typeof raw === "object" && !("outcomes" in raw) && "destination" in raw) raw.outcomes = [{
|
|
10538
|
-
action: "send_message",
|
|
10539
|
-
destination: raw.destination
|
|
10540
|
-
}];
|
|
10541
|
-
const retained = retainedEventAutomationSchema.parse(raw);
|
|
10542
|
-
const { threadTs: _threadTs, ...destination } = retained.destination;
|
|
10543
|
-
const title = row.title?.trim();
|
|
10544
|
-
return {
|
|
10545
|
-
...retained,
|
|
10546
|
-
destination,
|
|
10547
|
-
outcomes: retained.outcomes.map((outcome) => {
|
|
10548
|
-
const { threadTs: _threadTs, ...destination } = outcome.destination;
|
|
10549
|
-
return {
|
|
10550
|
-
...outcome,
|
|
10551
|
-
destination
|
|
10552
|
-
};
|
|
10553
|
-
}),
|
|
10554
|
-
status: row.status ?? "active",
|
|
10555
|
-
...title ? { title } : void 0
|
|
10556
|
-
};
|
|
10557
|
-
}
|
|
10558
|
-
function activeEventAutomationWhere() {
|
|
10559
|
-
return ne(juniorEventAutomations.status, "deleted");
|
|
10560
|
-
}
|
|
10561
|
-
/** Read one event automation by id, including deleted rows when present. */
|
|
10562
|
-
async function getEventAutomation(db, id) {
|
|
10563
|
-
const rows = await db.select({
|
|
10564
|
-
status: juniorEventAutomations.status,
|
|
10565
|
-
task: juniorEventAutomations.task,
|
|
10566
|
-
title: juniorEventAutomations.title
|
|
10567
|
-
}).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).limit(1);
|
|
10568
|
-
return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
|
|
10569
|
-
}
|
|
10570
|
-
/** Create one retry-stable event automation, or revive a deleted row with the new payload. */
|
|
10571
|
-
async function createEventAutomation(db, task) {
|
|
10572
|
-
const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
|
|
10573
|
-
const title = task.title?.trim() || null;
|
|
10574
|
-
return db.transaction(async (tx) => {
|
|
10575
|
-
const inserted = await tx.insert(juniorEventAutomations).values({
|
|
10576
|
-
id: parsed.id,
|
|
10577
|
-
teamId: parsed.destination.teamId,
|
|
10578
|
-
namespace: parsed.trigger.namespace,
|
|
10579
|
-
identifier: parsed.trigger.identifier,
|
|
10580
|
-
createdAtMs: parsed.createdAtMs,
|
|
10581
|
-
status: "active",
|
|
10582
|
-
title,
|
|
10583
|
-
task: parsed
|
|
10584
|
-
}).onConflictDoNothing().returning({
|
|
10585
|
-
status: juniorEventAutomations.status,
|
|
10586
|
-
task: juniorEventAutomations.task,
|
|
10587
|
-
title: juniorEventAutomations.title
|
|
10588
|
-
});
|
|
10589
|
-
if (inserted[0]) {
|
|
10590
|
-
const created = parseEventAutomationRow(inserted[0]);
|
|
10591
|
-
await recordAutomationVersion(tx, "event", created, void 0);
|
|
10592
|
-
return created;
|
|
10593
|
-
}
|
|
10594
|
-
const existing = await getEventAutomation(tx, parsed.id);
|
|
10595
|
-
if (!existing) return {
|
|
10596
|
-
...parsed,
|
|
10597
|
-
status: "active",
|
|
10598
|
-
...title ? { title } : void 0
|
|
10599
|
-
};
|
|
10600
|
-
if (existing.status !== "deleted") return existing;
|
|
10601
|
-
const rows = await tx.update(juniorEventAutomations).set({
|
|
10602
|
-
teamId: parsed.destination.teamId,
|
|
10603
|
-
namespace: parsed.trigger.namespace,
|
|
10604
|
-
identifier: parsed.trigger.identifier,
|
|
10605
|
-
status: "active",
|
|
10606
|
-
title,
|
|
10607
|
-
task: parsed
|
|
10608
|
-
}).where(and(eq(juniorEventAutomations.id, parsed.id), eq(juniorEventAutomations.status, "deleted"))).returning({
|
|
10609
|
-
status: juniorEventAutomations.status,
|
|
10610
|
-
task: juniorEventAutomations.task,
|
|
10611
|
-
title: juniorEventAutomations.title
|
|
10612
|
-
});
|
|
10613
|
-
if (!rows[0]) return await getEventAutomation(tx, parsed.id) ?? {
|
|
10614
|
-
...parsed,
|
|
10615
|
-
status: "active",
|
|
10616
|
-
...title ? { title } : void 0
|
|
10617
|
-
};
|
|
10618
|
-
const revived = parseEventAutomationRow(rows[0]);
|
|
10619
|
-
await recordAutomationVersion(tx, "event", revived, void 0);
|
|
10620
|
-
return revived;
|
|
10621
|
-
});
|
|
10622
|
-
}
|
|
10623
|
-
/**
|
|
10624
|
-
* Replace an existing non-deleted event automation under a row lock.
|
|
10625
|
-
* Pass `editedBy` when a person changes the definition so its version names them.
|
|
10626
|
-
*/
|
|
10627
|
-
async function saveEventAutomation(db, task, expectedRevision, editedBy) {
|
|
10628
|
-
const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
|
|
10629
|
-
const title = task.title?.trim() || null;
|
|
10630
|
-
return db.transaction(async (tx) => {
|
|
10631
|
-
const rows = await tx.select({
|
|
10632
|
-
status: juniorEventAutomations.status,
|
|
10633
|
-
task: juniorEventAutomations.task,
|
|
10634
|
-
title: juniorEventAutomations.title
|
|
10635
|
-
}).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, parsed.id)).for("update");
|
|
10636
|
-
const current = rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
|
|
10637
|
-
if (expectedRevision !== void 0) requireAutomationRevision(current, expectedRevision);
|
|
10638
|
-
if (!current || current.status === "deleted") return void 0;
|
|
10639
|
-
const saved = parseEventAutomationRow((await tx.update(juniorEventAutomations).set({
|
|
10640
|
-
namespace: parsed.trigger.namespace,
|
|
10641
|
-
identifier: parsed.trigger.identifier,
|
|
10642
|
-
title,
|
|
10643
|
-
task: parsed
|
|
10644
|
-
}).where(eq(juniorEventAutomations.id, parsed.id)).returning({
|
|
10645
|
-
status: juniorEventAutomations.status,
|
|
10646
|
-
task: juniorEventAutomations.task,
|
|
10647
|
-
title: juniorEventAutomations.title
|
|
10648
|
-
}))[0]);
|
|
10649
|
-
await recordAutomationVersion(tx, "event", saved, current, editedBy);
|
|
10650
|
-
return saved;
|
|
10651
|
-
});
|
|
10652
|
-
}
|
|
10653
|
-
/** Mark one existing event automation deleted while retaining the row for history. */
|
|
10654
|
-
async function deleteEventAutomation(db, id) {
|
|
10655
|
-
const rows = await db.update(juniorEventAutomations).set({ status: "deleted" }).where(and(eq(juniorEventAutomations.id, id), activeEventAutomationWhere())).returning({
|
|
10656
|
-
status: juniorEventAutomations.status,
|
|
10657
|
-
task: juniorEventAutomations.task,
|
|
10658
|
-
title: juniorEventAutomations.title
|
|
10659
|
-
});
|
|
10660
|
-
return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
|
|
10661
|
-
}
|
|
10662
|
-
/** List live event automations in one Slack workspace. */
|
|
10663
|
-
async function listEventAutomationsForTeam(db, teamId) {
|
|
10664
|
-
return (await db.select({
|
|
10665
|
-
status: juniorEventAutomations.status,
|
|
10666
|
-
task: juniorEventAutomations.task,
|
|
10667
|
-
title: juniorEventAutomations.title
|
|
10668
|
-
}).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), activeEventAutomationWhere())).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseEventAutomationRow);
|
|
10669
|
-
}
|
|
10670
|
-
function viewerSlackIdentities(user) {
|
|
10671
|
-
return user.identities.filter((identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId));
|
|
10672
|
-
}
|
|
10673
|
-
/** Return whether one event automation was created by a viewer-linked Slack identity. */
|
|
10674
|
-
function eventAutomationBelongsToUser(task, user) {
|
|
10675
|
-
return viewerSlackIdentities(user).some((identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId);
|
|
10676
|
-
}
|
|
10677
|
-
/**
|
|
10678
|
-
* List deleted event automations created by one user, newest first.
|
|
10679
|
-
* Used only to keep historical runs after the live automation list drops the row.
|
|
10680
|
-
*/
|
|
10681
|
-
async function listDeletedEventAutomationsCreatedBy(db, user, limit) {
|
|
10682
|
-
const identities = viewerSlackIdentities(user);
|
|
10683
|
-
const ownership = or(...identities.map((identity) => and(eq(juniorEventAutomations.teamId, identity.providerTenantId), sql`${juniorEventAutomations.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
|
|
10684
|
-
if (!ownership) return [];
|
|
10685
|
-
return (await db.select({
|
|
10686
|
-
status: juniorEventAutomations.status,
|
|
10687
|
-
task: juniorEventAutomations.task,
|
|
10688
|
-
title: juniorEventAutomations.title
|
|
10689
|
-
}).from(juniorEventAutomations).where(and(ownership, eq(juniorEventAutomations.status, "deleted"))).orderBy(desc(juniorEventAutomations.createdAtMs), desc(juniorEventAutomations.id)).limit(limit)).map(parseEventAutomationRow);
|
|
10690
|
-
}
|
|
10691
|
-
/**
|
|
10692
|
-
* Collect match keys from live event automations for these identifiers and event types.
|
|
10693
|
-
* Does not evaluate match values — only reports keys that filters use.
|
|
10694
|
-
*/
|
|
10695
|
-
async function collectEventAutomationMatchKeys(db, input) {
|
|
10696
|
-
const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
|
|
10697
|
-
const identifiers = [...new Set(input.identifiers.map((value) => value.trim()).filter(Boolean))];
|
|
10698
|
-
if (eventTypes.size === 0 || identifiers.length === 0) return [];
|
|
10699
|
-
const rows = await db.select({
|
|
10700
|
-
status: juniorEventAutomations.status,
|
|
10701
|
-
task: juniorEventAutomations.task,
|
|
10702
|
-
title: juniorEventAutomations.title
|
|
10703
|
-
}).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, input.teamId), eq(juniorEventAutomations.namespace, input.namespace), inArray(juniorEventAutomations.identifier, identifiers), eq(juniorEventAutomations.status, "active"))).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id));
|
|
10704
|
-
const keys = /* @__PURE__ */ new Set();
|
|
10705
|
-
for (const row of rows) {
|
|
10706
|
-
const task = parseEventAutomationRow(row);
|
|
10707
|
-
if (!task.trigger.events.some((eventType) => eventTypes.has(eventType))) continue;
|
|
10708
|
-
for (const key of Object.keys(task.trigger.match ?? {})) keys.add(key);
|
|
10709
|
-
}
|
|
10710
|
-
return [...keys].sort();
|
|
10711
|
-
}
|
|
10712
|
-
/** Find every live task matching one normalized event. */
|
|
10713
|
-
async function findMatchingEventAutomations(db, event, teamId) {
|
|
10714
|
-
return (await db.select({
|
|
10715
|
-
status: juniorEventAutomations.status,
|
|
10716
|
-
task: juniorEventAutomations.task,
|
|
10717
|
-
title: juniorEventAutomations.title
|
|
10718
|
-
}).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), eq(juniorEventAutomations.namespace, event.namespace), eq(juniorEventAutomations.identifier, event.identifier), eq(juniorEventAutomations.status, "active"))).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseEventAutomationRow).filter((task) => task.trigger.events.includes(event.eventType) && eventMatches(task.trigger.match, event.data));
|
|
10719
|
-
}
|
|
10720
|
-
/** Change lifecycle under the same row lock as edits, without changing credentials. */
|
|
10721
|
-
async function setEventAutomationStatus(db, id, status, revision) {
|
|
10722
|
-
return db.transaction(async (tx) => {
|
|
10723
|
-
const rows = await tx.select().from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).for("update");
|
|
10724
|
-
const current = rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
|
|
10725
|
-
requireAutomationRevision(current, revision);
|
|
10726
|
-
if (!current || current.status === "deleted") throw new Error("Automation no longer exists.");
|
|
10727
|
-
await tx.update(juniorEventAutomations).set({ status }).where(eq(juniorEventAutomations.id, id));
|
|
10728
|
-
return {
|
|
10729
|
-
...current,
|
|
10730
|
-
status
|
|
10731
|
-
};
|
|
10732
|
-
});
|
|
10733
|
-
}
|
|
10734
|
-
//#endregion
|
|
10735
10101
|
//#region src/chat/task-outcomes.ts
|
|
10736
10102
|
/** Resolve explicit message outcomes to the Slack Destinations stored on an Automation. */
|
|
10737
10103
|
async function resolveTaskOutcomes(outcomes, currentDestination, creatorSlackUserId) {
|
|
@@ -10745,9 +10111,7 @@ async function resolveTaskOutcomes(outcomes, currentDestination, creatorSlackUse
|
|
|
10745
10111
|
});
|
|
10746
10112
|
continue;
|
|
10747
10113
|
}
|
|
10748
|
-
const
|
|
10749
|
-
const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
|
|
10750
|
-
if (!channelId) throw new Error("Slack did not return a direct message destination.");
|
|
10114
|
+
const channelId = await openSlackDirectMessage(creatorSlackUserId);
|
|
10751
10115
|
resolved.push({
|
|
10752
10116
|
action: "send_message",
|
|
10753
10117
|
destination: {
|
|
@@ -10782,9 +10146,11 @@ const compactEventAutomationResultSchema = z.object({
|
|
|
10782
10146
|
id: z.string().min(1),
|
|
10783
10147
|
status: z.enum([
|
|
10784
10148
|
"active",
|
|
10149
|
+
"blocked",
|
|
10785
10150
|
"paused",
|
|
10786
10151
|
"deleted"
|
|
10787
10152
|
]),
|
|
10153
|
+
statusReason: z.string().nullable(),
|
|
10788
10154
|
title: z.string().min(1).nullable(),
|
|
10789
10155
|
dashboardUrl: z.string().url().nullable(),
|
|
10790
10156
|
instruction: z.string().min(1),
|
|
@@ -10885,6 +10251,7 @@ function compactEventAutomation(task, catalog, requesterSlackUserId) {
|
|
|
10885
10251
|
return compactEventAutomationResultSchema.parse({
|
|
10886
10252
|
id: task.id,
|
|
10887
10253
|
status: task.status,
|
|
10254
|
+
statusReason: task.statusReason ?? null,
|
|
10888
10255
|
title: task.title?.trim() || null,
|
|
10889
10256
|
dashboardUrl: getDashboardTaskLink(task.id) ?? null,
|
|
10890
10257
|
instruction: task.task.text,
|
|
@@ -11173,23 +10540,25 @@ function createUpdateEventAutomationTool(context, catalog) {
|
|
|
11173
10540
|
readOnlyHint: false
|
|
11174
10541
|
},
|
|
11175
10542
|
executionMode: "sequential",
|
|
11176
|
-
description: "Update the instruction, registered trigger,
|
|
10543
|
+
description: "Update the instruction, registered trigger, credential use, or status of an event automation.",
|
|
11177
10544
|
inputSchema: z.object({
|
|
11178
10545
|
automationId: z.string().min(1),
|
|
11179
10546
|
title: automationTitleSchema.optional(),
|
|
11180
10547
|
instruction: automationInstructionToolSchema.nullable().optional(),
|
|
11181
10548
|
trigger: registeredEventAutomationTriggerSchema(catalog).nullable().optional(),
|
|
11182
10549
|
outcomes: z.array(taskOutcomeInputSchema).max(5).nullable().describe("Replacement messages to send after successful work. Use an empty list to send nothing. Omit or use null to leave unchanged.").optional(),
|
|
11183
|
-
credentialMode: z.enum(["system", "creator"]).nullable().describe("Set creator to make the task's original creator credentials available, or system to disable them. Creator always means the task's createdBy actor, never the current requester. Only that original creator may enable creator mode. Omit or use null to leave unchanged.").optional()
|
|
10550
|
+
credentialMode: z.enum(["system", "creator"]).nullable().describe("Set creator to make the task's original creator credentials available, or system to disable them. Creator always means the task's createdBy actor, never the current requester. Only that original creator may enable creator mode. Omit or use null to leave unchanged.").optional(),
|
|
10551
|
+
status: z.enum(["active", "paused"]).nullable().describe("Set active to resume a paused or blocked automation, or paused to stop it. Only the creator can change the status. Omit or use null to leave unchanged.").optional()
|
|
11184
10552
|
}).strict(),
|
|
11185
10553
|
prepareArguments(args) {
|
|
11186
|
-
const { credentialMode, outcomes, instruction, trigger, ...prepared } = args;
|
|
10554
|
+
const { credentialMode, outcomes, instruction, trigger, status, ...prepared } = args;
|
|
11187
10555
|
return {
|
|
11188
10556
|
...prepared,
|
|
11189
10557
|
...instruction != null ? { instruction } : void 0,
|
|
11190
10558
|
...trigger != null ? { trigger } : void 0,
|
|
11191
10559
|
...outcomes != null ? { outcomes } : void 0,
|
|
11192
|
-
...credentialMode != null ? { credentialMode } : void 0
|
|
10560
|
+
...credentialMode != null ? { credentialMode } : void 0,
|
|
10561
|
+
...status != null ? { status } : void 0
|
|
11193
10562
|
};
|
|
11194
10563
|
},
|
|
11195
10564
|
outputSchema: eventAutomationToolResultSchema,
|
|
@@ -11197,21 +10566,28 @@ function createUpdateEventAutomationTool(context, catalog) {
|
|
|
11197
10566
|
const current = await writableEventAutomation(context, input.automationId);
|
|
11198
10567
|
const { actor } = requireEventAutomationSlackContext(context);
|
|
11199
10568
|
const isCreator = actor.userId === current.createdBy.slackUserId;
|
|
11200
|
-
|
|
10569
|
+
const edits = input.title !== void 0 || input.instruction !== void 0 || input.trigger !== void 0 || input.outcomes != null || input.credentialMode != null;
|
|
10570
|
+
if (!edits && input.status == null) throw new ToolInputError("Event automation update requires a change.");
|
|
10571
|
+
if (input.status != null && !isCreator) throw new ToolInputError("Only the creator of this event automation can pause or resume it.");
|
|
11201
10572
|
if (input.instruction != null && input.instruction !== current.task.text) requireRequesterMention(input.instruction, actor);
|
|
11202
|
-
|
|
11203
|
-
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
10573
|
+
let saved = current;
|
|
10574
|
+
if (edits) {
|
|
10575
|
+
const next = await editEventAutomation(current, {
|
|
10576
|
+
title: input.title,
|
|
10577
|
+
instruction: input.instruction ?? void 0,
|
|
10578
|
+
trigger: input.trigger ?? void 0,
|
|
10579
|
+
outcomes: input.outcomes ?? void 0,
|
|
10580
|
+
credentialMode: input.credentialMode ?? void 0
|
|
10581
|
+
}, isCreator, catalog);
|
|
10582
|
+
const edited = await saveEventAutomation(getDb(), next, automationRevision(current), {
|
|
10583
|
+
slackUserId: actor.userId,
|
|
10584
|
+
...actor.fullName ? { fullName: actor.fullName } : void 0,
|
|
10585
|
+
...actor.userName ? { userName: actor.userName } : void 0
|
|
10586
|
+
});
|
|
10587
|
+
if (!edited) throw new ToolInputError("Event automation was not found.");
|
|
10588
|
+
saved = edited;
|
|
10589
|
+
}
|
|
10590
|
+
if (input.status != null) saved = await setEventAutomationStatus(getDb(), saved.id, input.status, automationRevision(saved));
|
|
11215
10591
|
return eventAutomationToolResult(context.conversationId, saved, catalog, actor.userId);
|
|
11216
10592
|
}
|
|
11217
10593
|
});
|
|
@@ -11229,193 +10605,6 @@ function createEventAutomationTools(context, catalog) {
|
|
|
11229
10605
|
};
|
|
11230
10606
|
}
|
|
11231
10607
|
//#endregion
|
|
11232
|
-
//#region src/chat/scheduled-automations/tasks.ts
|
|
11233
|
-
const SCHEDULER_KEY_PREFIX = "junior:scheduler";
|
|
11234
|
-
const retainedScheduledAutomationSchema = scheduledAutomationSchema.omit({
|
|
11235
|
-
creatorIdentityId: true,
|
|
11236
|
-
title: true
|
|
11237
|
-
}).extend({
|
|
11238
|
-
destination: slackDestinationSchema,
|
|
11239
|
-
outcomes: z.array(taskOutcomeSchema).max(5),
|
|
11240
|
-
version: z.number().optional()
|
|
11241
|
-
}).strict();
|
|
11242
|
-
/** Decode a retained scheduler task row and reject invalid routing context. */
|
|
11243
|
-
function parseScheduledAutomationRow(row) {
|
|
11244
|
-
const record = z.record(z.string(), z.unknown()).safeParse(row.record);
|
|
11245
|
-
if (!record.success) return void 0;
|
|
11246
|
-
const { creatorIdentityId: legacyCreatorIdentityId, title: legacyTitle, ...retained } = record.data;
|
|
11247
|
-
const current = retained.outcomes === void 0 && retained.destination !== void 0 ? {
|
|
11248
|
-
...retained,
|
|
11249
|
-
outcomes: [{
|
|
11250
|
-
action: "send_message",
|
|
11251
|
-
destination: retained.destination
|
|
11252
|
-
}]
|
|
11253
|
-
} : retained;
|
|
11254
|
-
const parsed = retainedScheduledAutomationSchema.safeParse(current);
|
|
11255
|
-
if (!parsed.success) return void 0;
|
|
11256
|
-
const { status, version: _version, ...retainedTask } = parsed.data;
|
|
11257
|
-
const destination = {
|
|
11258
|
-
platform: "slack",
|
|
11259
|
-
teamId: retainedTask.destination.teamId,
|
|
11260
|
-
channelId: retainedTask.destination.channelId
|
|
11261
|
-
};
|
|
11262
|
-
const task = {
|
|
11263
|
-
...retainedTask,
|
|
11264
|
-
destination,
|
|
11265
|
-
outcomes: retainedTask.outcomes.map((outcome) => ({
|
|
11266
|
-
...outcome,
|
|
11267
|
-
destination: {
|
|
11268
|
-
platform: "slack",
|
|
11269
|
-
teamId: outcome.destination.teamId,
|
|
11270
|
-
channelId: outcome.destination.channelId
|
|
11271
|
-
}
|
|
11272
|
-
}))
|
|
11273
|
-
};
|
|
11274
|
-
const fallbackIdentity = row.creatorIdentityId === null ? z.string().safeParse(legacyCreatorIdentityId) : void 0;
|
|
11275
|
-
const creatorIdentityId = row.creatorIdentityId ?? (fallbackIdentity?.success ? fallbackIdentity.data : void 0);
|
|
11276
|
-
if (creatorIdentityId === void 0) return void 0;
|
|
11277
|
-
const fallbackTitle = row.title === null ? z.string().safeParse(legacyTitle) : void 0;
|
|
11278
|
-
const title = (row.title !== null ? row.title : fallbackTitle?.success ? fallbackTitle.data : void 0)?.trim() || void 0;
|
|
11279
|
-
return {
|
|
11280
|
-
...task,
|
|
11281
|
-
creatorIdentityId,
|
|
11282
|
-
status,
|
|
11283
|
-
...title ? { title } : void 0
|
|
11284
|
-
};
|
|
11285
|
-
}
|
|
11286
|
-
/** Keep scan paths from returning current or legacy tombstones. */
|
|
11287
|
-
function isListedScheduledAutomation(task) {
|
|
11288
|
-
return task !== void 0 && task.status !== "deleted";
|
|
11289
|
-
}
|
|
11290
|
-
/** Build the stable run id for one task occurrence. */
|
|
11291
|
-
function scheduledRunId(taskId, scheduledForMs) {
|
|
11292
|
-
return `${taskId}:${scheduledForMs}`;
|
|
11293
|
-
}
|
|
11294
|
-
/** Acquire the deployed advisory lock for one scheduled automation. */
|
|
11295
|
-
function withScheduledAutomationLock(db, taskId, callback) {
|
|
11296
|
-
return db.transaction(async (tx) => {
|
|
11297
|
-
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`${SCHEDULER_KEY_PREFIX}:task:${taskId}:lock`}))`);
|
|
11298
|
-
return await callback(tx);
|
|
11299
|
-
});
|
|
11300
|
-
}
|
|
11301
|
-
function requireStoredTask(task) {
|
|
11302
|
-
const parsed = scheduledAutomationSchema.safeParse(task);
|
|
11303
|
-
if (!parsed.success) throw new Error("Scheduled automation routing context is invalid.");
|
|
11304
|
-
const { title, ...current } = parsed.data;
|
|
11305
|
-
const normalizedTitle = title?.trim() || void 0;
|
|
11306
|
-
return {
|
|
11307
|
-
...current,
|
|
11308
|
-
...normalizedTitle ? { title: normalizedTitle } : void 0
|
|
11309
|
-
};
|
|
11310
|
-
}
|
|
11311
|
-
function scheduledAutomationJsonRecord(task) {
|
|
11312
|
-
const { title: _title, ...record } = task;
|
|
11313
|
-
return record;
|
|
11314
|
-
}
|
|
11315
|
-
async function upsertScheduledAutomation(db, task) {
|
|
11316
|
-
const title = task.title?.trim() || null;
|
|
11317
|
-
await db.insert(juniorSchedulerTasks).values({
|
|
11318
|
-
createdAtMs: task.createdAtMs,
|
|
11319
|
-
creatorIdentityId: task.creatorIdentityId,
|
|
11320
|
-
creatorSlackUserId: task.createdBy.slackUserId,
|
|
11321
|
-
id: task.id,
|
|
11322
|
-
nextRunAtMs: task.nextRunAtMs,
|
|
11323
|
-
record: scheduledAutomationJsonRecord(task),
|
|
11324
|
-
runNowAtMs: task.runNowAtMs,
|
|
11325
|
-
status: task.status,
|
|
11326
|
-
teamId: task.destination.teamId,
|
|
11327
|
-
title
|
|
11328
|
-
}).onConflictDoUpdate({
|
|
11329
|
-
target: juniorSchedulerTasks.id,
|
|
11330
|
-
set: {
|
|
11331
|
-
createdAtMs: sql`excluded.created_at_ms`,
|
|
11332
|
-
creatorIdentityId: sql`excluded.creator_identity_id`,
|
|
11333
|
-
creatorSlackUserId: sql`excluded.creator_slack_user_id`,
|
|
11334
|
-
nextRunAtMs: sql`excluded.next_run_at_ms`,
|
|
11335
|
-
record: sql`excluded.record`,
|
|
11336
|
-
runNowAtMs: sql`excluded.run_now_at_ms`,
|
|
11337
|
-
status: sql`excluded.status`,
|
|
11338
|
-
teamId: sql`excluded.team_id`,
|
|
11339
|
-
title: sql`excluded.title`
|
|
11340
|
-
}
|
|
11341
|
-
});
|
|
11342
|
-
}
|
|
11343
|
-
/** Read and decode one scheduled automation. */
|
|
11344
|
-
async function readScheduledAutomation(db, taskId) {
|
|
11345
|
-
const rows = await db.select({
|
|
11346
|
-
creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
|
|
11347
|
-
record: juniorSchedulerTasks.record,
|
|
11348
|
-
title: juniorSchedulerTasks.title
|
|
11349
|
-
}).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
|
|
11350
|
-
return rows[0] ? parseScheduledAutomationRow(rows[0]) : void 0;
|
|
11351
|
-
}
|
|
11352
|
-
async function readListedScheduledAutomations(db, teamId) {
|
|
11353
|
-
return (await db.select({
|
|
11354
|
-
creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
|
|
11355
|
-
record: juniorSchedulerTasks.record,
|
|
11356
|
-
title: juniorSchedulerTasks.title
|
|
11357
|
-
}).from(juniorSchedulerTasks).where(and(notInArray(juniorSchedulerTasks.status, ["deleted"]), teamId === void 0 ? void 0 : eq(juniorSchedulerTasks.teamId, teamId))).orderBy(asc(juniorSchedulerTasks.createdAtMs), asc(juniorSchedulerTasks.id))).map(parseScheduledAutomationRow).filter(isListedScheduledAutomation);
|
|
11358
|
-
}
|
|
11359
|
-
/** List decoded scheduled automations in stable creation order. */
|
|
11360
|
-
async function listScheduledAutomations(db) {
|
|
11361
|
-
return await readListedScheduledAutomations(db);
|
|
11362
|
-
}
|
|
11363
|
-
/** List decoded scheduled automations for one team in stable creation order. */
|
|
11364
|
-
async function listScheduledAutomationsForTeam(db, teamId) {
|
|
11365
|
-
return await readListedScheduledAutomations(db, teamId);
|
|
11366
|
-
}
|
|
11367
|
-
async function writeScheduledAutomation(db, task, current, editedBy) {
|
|
11368
|
-
if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) await db.delete(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.id, scheduledRunId(task.id, task.nextRunAtMs)), eq(juniorSchedulerRuns.status, "blocked")));
|
|
11369
|
-
if (task.status === "deleted" && current?.status !== "deleted") await skipPendingRunsForDeletedTask(db, task);
|
|
11370
|
-
await upsertScheduledAutomation(db, task);
|
|
11371
|
-
await recordAutomationVersion(db, "scheduled", task, current, editedBy);
|
|
11372
|
-
}
|
|
11373
|
-
async function skipPendingRunsForDeletedTask(db, task) {
|
|
11374
|
-
const errorMessage = `Scheduled automation ${task.id} was deleted before the run started.`;
|
|
11375
|
-
const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.taskId, task.id), eq(juniorSchedulerRuns.status, "pending")));
|
|
11376
|
-
for (const row of rows) {
|
|
11377
|
-
const parsed = scheduledRunSchema.safeParse(row.record);
|
|
11378
|
-
if (!parsed.success || parsed.data.status !== "pending") continue;
|
|
11379
|
-
const next = scheduledRunSchema.parse({
|
|
11380
|
-
...parsed.data,
|
|
11381
|
-
completedAtMs: task.updatedAtMs,
|
|
11382
|
-
errorMessage,
|
|
11383
|
-
status: "skipped"
|
|
11384
|
-
});
|
|
11385
|
-
await db.update(juniorSchedulerRuns).set({
|
|
11386
|
-
record: next,
|
|
11387
|
-
status: next.status
|
|
11388
|
-
}).where(and(eq(juniorSchedulerRuns.id, next.id), eq(juniorSchedulerRuns.status, "pending")));
|
|
11389
|
-
}
|
|
11390
|
-
}
|
|
11391
|
-
/** Create a scheduled automation once under its retry-stable task lock. */
|
|
11392
|
-
async function createScheduledAutomation(db, task) {
|
|
11393
|
-
const next = requireStoredTask(task);
|
|
11394
|
-
return await withScheduledAutomationLock(db, task.id, async (tx) => {
|
|
11395
|
-
const current = await readScheduledAutomation(tx, task.id);
|
|
11396
|
-
if (current) return current;
|
|
11397
|
-
await writeScheduledAutomation(tx, next, void 0);
|
|
11398
|
-
return next;
|
|
11399
|
-
});
|
|
11400
|
-
}
|
|
11401
|
-
/**
|
|
11402
|
-
* Save a scheduled automation and clear its blocked occurrence on reactivation.
|
|
11403
|
-
* Pass `editedBy` when a person changes the definition so its version names them.
|
|
11404
|
-
*/
|
|
11405
|
-
async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
|
|
11406
|
-
const next = requireStoredTask(task);
|
|
11407
|
-
await withScheduledAutomationLock(db, task.id, async (tx) => {
|
|
11408
|
-
const current = await readScheduledAutomation(tx, task.id);
|
|
11409
|
-
if (expectedRevision !== void 0) requireAutomationRevision(current, expectedRevision);
|
|
11410
|
-
await writeScheduledAutomation(tx, next, current, editedBy);
|
|
11411
|
-
});
|
|
11412
|
-
return next;
|
|
11413
|
-
}
|
|
11414
|
-
/** Save a scheduled automation inside a task lock already held by the caller. */
|
|
11415
|
-
async function saveScheduledAutomationInLock(db, task, current) {
|
|
11416
|
-
await writeScheduledAutomation(db, requireStoredTask(task), current);
|
|
11417
|
-
}
|
|
11418
|
-
//#endregion
|
|
11419
10608
|
//#region src/chat/scheduled-automations/telemetry.ts
|
|
11420
10609
|
/** Safe structured telemetry attributes for scheduled-automation lifecycle events. */
|
|
11421
10610
|
/** Correlation attributes shared by scheduled-automation create and run events. */
|
|
@@ -11772,7 +10961,7 @@ function createSlackScheduleCreateAutomationTool(context) {
|
|
|
11772
10961
|
instruction: input.instruction,
|
|
11773
10962
|
title: input.title
|
|
11774
10963
|
});
|
|
11775
|
-
const
|
|
10964
|
+
const task = {
|
|
11776
10965
|
id,
|
|
11777
10966
|
createdAtMs: nowMs,
|
|
11778
10967
|
updatedAtMs: nowMs,
|
|
@@ -11789,7 +10978,8 @@ function createSlackScheduleCreateAutomationTool(context) {
|
|
|
11789
10978
|
outcomes: await resolveTaskOutcomes(input.outcomes, destination, actor.slackUserId),
|
|
11790
10979
|
task: { text: input.instruction },
|
|
11791
10980
|
...title ? { title } : void 0
|
|
11792
|
-
}
|
|
10981
|
+
};
|
|
10982
|
+
const committed = await createScheduledAutomation(db, task);
|
|
11793
10983
|
if (!sameDestination(committed, destination) || committed.createdBy.slackUserId !== actor.slackUserId) throwToolInputError("Scheduled automation operation identity is invalid.");
|
|
11794
10984
|
logInfo("scheduled_automation.create.completed", scheduledAutomationAttributes(committed));
|
|
11795
10985
|
return scheduleAutomationToolResult(context.conversationId, committed, actor.slackUserId);
|
|
@@ -15036,7 +14226,7 @@ async function resolveStoredProvider(teamId, provider, query) {
|
|
|
15036
14226
|
/** Create the tool that resolves people by identity provider and query. */
|
|
15037
14227
|
function createUserLookupTool(teamId, providers) {
|
|
15038
14228
|
return zodTool({
|
|
15039
|
-
description: "Look up people by identity provider and query. Pass an enabled `provider` and its provider subject ID or handle. The `slack` provider also accepts email and name searches. Returns Slack `mention` values (`<@U…>`) for the current workspace. Use a mention only when one person clearly matches; otherwise ask which person the user means.",
|
|
14229
|
+
description: "Look up people by identity provider and query. Pass an enabled `provider` and its provider subject ID or handle. For `slack`, pass the person's Slack user ID (`U…`) whenever the conversation shows one, such as the requester's `author_id`, a participant's `actor_id`, or a `<@U…>` mention. The `slack` provider also accepts email and name searches. Use name search only for people with no known ID, because it can miss people in large workspaces. Returns Slack `mention` values (`<@U…>`) for the current workspace. Use a mention only when one person clearly matches; otherwise ask which person the user means.",
|
|
15040
14230
|
annotations: {
|
|
15041
14231
|
destructiveHint: false,
|
|
15042
14232
|
idempotentHint: true,
|
|
@@ -15045,7 +14235,7 @@ function createUserLookupTool(teamId, providers) {
|
|
|
15045
14235
|
},
|
|
15046
14236
|
inputSchema: z.object({
|
|
15047
14237
|
provider: z.enum(providers).describe("Enabled identity provider to query."),
|
|
15048
|
-
query: z.string().trim().min(1).describe("Provider subject ID, handle, Slack email, or Slack name.")
|
|
14238
|
+
query: z.string().trim().min(1).describe("Provider subject ID, handle, Slack email, or Slack name. Prefer a Slack user ID (U…) when one is known.")
|
|
15049
14239
|
}).strict(),
|
|
15050
14240
|
outputSchema: juniorToolOutputSchema,
|
|
15051
14241
|
execute: async ({ provider, query }) => {
|
|
@@ -16939,6 +16129,7 @@ function createTools(availableSkills, hooks = {}, context, options = {}) {
|
|
|
16939
16129
|
}
|
|
16940
16130
|
if (context.handoff) tools.handoff = createHandoffTool(context.handoff);
|
|
16941
16131
|
if (context.spawnAgent) tools.spawnAgent = createSpawnAgentTool(context.spawnAgent);
|
|
16132
|
+
if (context.automation) tools[FINISH_AUTOMATION_RUN_TOOL_NAME] = createFinishAutomationRunTool(context.automation);
|
|
16942
16133
|
if (context.mcpToolManager) {
|
|
16943
16134
|
tools.searchMcpTools = createSearchMcpToolsTool(context.mcpToolManager);
|
|
16944
16135
|
tools.callMcpTool = createCallMcpToolTool(context.mcpToolManager);
|
|
@@ -19894,7 +19085,8 @@ async function wireAgentTools(args) {
|
|
|
19894
19085
|
surface: args.surface,
|
|
19895
19086
|
...args.currentActor ? { resolveActorIdentity: async () => await readActorIdentity(args.currentActor) } : void 0,
|
|
19896
19087
|
...args.durability.spawnAgent ? { spawnAgent: args.durability.spawnAgent } : void 0,
|
|
19897
|
-
...args.requestHandoff ? { handoff: args.requestHandoff } : void 0
|
|
19088
|
+
...args.requestHandoff ? { handoff: args.requestHandoff } : void 0,
|
|
19089
|
+
...args.run.dispatch?.declaresResult ? { automation: { sendsMessage: args.run.dispatch.outcomes?.length !== 0 } } : void 0
|
|
19898
19090
|
};
|
|
19899
19091
|
const toolRoute = resolveToolRuntimeRoute({
|
|
19900
19092
|
actor: args.currentActor,
|
|
@@ -20727,7 +19919,11 @@ async function compactActiveContextIfNeeded(args, deps) {
|
|
|
20727
19919
|
//#endregion
|
|
20728
19920
|
//#region src/chat/agent/handoff.ts
|
|
20729
19921
|
/** Tools that must be the only tool call in their assistant message. */
|
|
20730
|
-
const EXCLUSIVE_TOOL_NAMES = [
|
|
19922
|
+
const EXCLUSIVE_TOOL_NAMES = [
|
|
19923
|
+
HANDOFF_TOOL_NAME,
|
|
19924
|
+
"switchWorkspace",
|
|
19925
|
+
FINISH_AUTOMATION_RUN_TOOL_NAME
|
|
19926
|
+
];
|
|
20731
19927
|
/** Return the exclusive tool name when one is present among the calls. */
|
|
20732
19928
|
function exclusiveToolName(toolCalls) {
|
|
20733
19929
|
for (const name of EXCLUSIVE_TOOL_NAMES) if (toolCalls.some((call) => call.name === name)) return name;
|
|
@@ -20926,6 +20122,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
20926
20122
|
const conversationId = run.conversationId;
|
|
20927
20123
|
const turnId = run.turnId;
|
|
20928
20124
|
const runId = run.runId;
|
|
20125
|
+
const requireAutomationResult = run.dispatch?.declaresResult === true;
|
|
20929
20126
|
const input = {
|
|
20930
20127
|
actor: run.instruction.actor,
|
|
20931
20128
|
includeConversationContextWithPiMessages: run.instruction.includeConversationContextWithHistory,
|
|
@@ -21450,11 +20647,13 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
21450
20647
|
};
|
|
21451
20648
|
}
|
|
21452
20649
|
},
|
|
21453
|
-
afterToolCall: async ({ result, toolCall }, signal) => {
|
|
20650
|
+
afterToolCall: async ({ isError, result, toolCall }, signal) => {
|
|
21454
20651
|
const timedOutResult = runResume.timedOut && signal?.aborted ? projectTimedOutToolResult(result) : void 0;
|
|
21455
20652
|
const sourceResult = timedOutResult ?? result;
|
|
21456
20653
|
const projectedResult = wiring.projectActionReviewResult(toolCall.id, sourceResult);
|
|
21457
|
-
|
|
20654
|
+
const override = timedOutResult || projectedResult !== result ? projectedResult : void 0;
|
|
20655
|
+
if (toolCall.name === "finishAutomationRun" && !isError && !override) return { terminate: true };
|
|
20656
|
+
return override;
|
|
21458
20657
|
},
|
|
21459
20658
|
prepareNextTurnWithContext: async (nextTurn, hookSignal) => {
|
|
21460
20659
|
try {
|
|
@@ -21503,6 +20702,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
21503
20702
|
runResume.setBeforeMessageCount(agent.state.messages.length);
|
|
21504
20703
|
if (shouldPromptAgent) runResume.setTurnStartMessageIndex(agent.state.messages.length);
|
|
21505
20704
|
const authPauseOutcome = await withSpan(`invoke_agent ${botConfig.userName}`, "gen_ai.invoke_agent", spanContext, async () => {
|
|
20705
|
+
if (requireAutomationResult && readAutomationResult(agent.state.messages)) return;
|
|
21506
20706
|
const promptTimestamp = restoredPromptTimestamp ?? Date.now();
|
|
21507
20707
|
const contextMessage = shouldPromptAgent && promptContextContentParts.length > 0 ? {
|
|
21508
20708
|
role: "user",
|
|
@@ -21620,6 +20820,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
21620
20820
|
run = shouldPromptAgent && !capacityUpdate ? agent.prompt(freshPromptMessage) : agent.continue();
|
|
21621
20821
|
let discardedRetryUsage;
|
|
21622
20822
|
let actionConfirmationRetryUsed = false;
|
|
20823
|
+
let automationResultReminderUsed = !requireAutomationResult;
|
|
21623
20824
|
let providerRetryAttempt = 0;
|
|
21624
20825
|
let emptyOutputAttempt = 0;
|
|
21625
20826
|
const prepareRetry = async (messages) => {
|
|
@@ -21659,6 +20860,15 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
21659
20860
|
run = agent.continue();
|
|
21660
20861
|
continue;
|
|
21661
20862
|
}
|
|
20863
|
+
const automationResultReminder = automationResultReminderUsed ? void 0 : remindMissingAutomationResult(agent.state.messages);
|
|
20864
|
+
if (automationResultReminder) {
|
|
20865
|
+
automationResultReminderUsed = true;
|
|
20866
|
+
agent.state.messages = automationResultReminder;
|
|
20867
|
+
await runResume.persistSafeBoundary(automationResultReminder);
|
|
20868
|
+
logWarn("agent.turn.automation_result.reminded", {});
|
|
20869
|
+
run = agent.continue();
|
|
20870
|
+
continue;
|
|
20871
|
+
}
|
|
21662
20872
|
const emptyOutputContinuation = nextEmptyOutputContinuation({
|
|
21663
20873
|
attempt: emptyOutputAttempt,
|
|
21664
20874
|
lastAssistant,
|
|
@@ -21725,7 +20935,8 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
21725
20935
|
usage: turnUsage,
|
|
21726
20936
|
executionProfile: turnRoute,
|
|
21727
20937
|
assistantUserName: botConfig.userName,
|
|
21728
|
-
modelId: activeModelId
|
|
20938
|
+
modelId: activeModelId,
|
|
20939
|
+
requireAutomationResult
|
|
21729
20940
|
})
|
|
21730
20941
|
};
|
|
21731
20942
|
} catch (error) {
|
|
@@ -21818,7 +21029,7 @@ function createAgentRunner(execute, options) {
|
|
|
21818
21029
|
} } : void 0
|
|
21819
21030
|
};
|
|
21820
21031
|
if (run.instruction.storedAttachments?.length) {
|
|
21821
|
-
const { loadInputImages } = await import("./images-
|
|
21032
|
+
const { loadInputImages } = await import("./images-DSxZt5vk.js").then((n) => n.n);
|
|
21822
21033
|
const storage = nextRun.environment?.attachmentStorage;
|
|
21823
21034
|
if (!storage) throw new Error("Attachment storage is unavailable.");
|
|
21824
21035
|
nextRun.instruction = {
|
|
@@ -21834,4 +21045,4 @@ function createAgentRunner(execute, options) {
|
|
|
21834
21045
|
} };
|
|
21835
21046
|
}
|
|
21836
21047
|
//#endregion
|
|
21837
|
-
export {
|
|
21048
|
+
export { getEventCatalog as $, actorFromRun as $t, resolveSlackChannelIdFromMessage as A, hasConversationStop as At, effectiveTaskOutcomes as B, SANDBOX_EGRESS_PROXY_PATH as Bt, renderBlockText as C, removeActiveConversation as Ct, storeAttachment as D, stopConversationWork as Dt, requestAttachmentDeletion as E, startConversationWork as Et, logScheduledAutomationRunSkipped as F, maybePostAutomatedTurnLimitNotice as Ft, AgentInvocationBusyError as G, sandboxEgressSignalsResponseSchema as Gt, agentBindingSchema as H, consumeSandboxEgressAuthRequiredSignal as Ht, scheduledAutomationRunAttributes as I, recordFinishedTurnForAutomatedLimit as It, processWorkspaceSnapshotJob as J, resolveBaseUrl as Jt, AgentInvocationLimitError as K, formatProviderLabel as Kt, editEventAutomation as L, resolveSandboxEgressProviderForHost as Lt, getConversationInfo as M, isFinalAttempt as Mt, listThreadReplies as N, isInvalidConversationRecordError as Nt, parseSlackThreadId as O, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as Ot, editScheduledAutomation as P, renderTaskInput as Pt, timerWatchCredentialSubject as Q, RetryableDeliveryError as Qt, AutomationEditError as R, normalizeSandboxEgressTracePropagationDomains as Rt, renderAttachmentText as S, releaseConversationWork as St, readLiveAttachment as T, requestConversationWork as Tt, agentInvocationSchema as U, consumeSandboxEgressPermissionDeniedSignal as Ut, resolveTaskOutcomes as V, clearSandboxEgressSignals as Vt, createAgentInvocationSchema as W, parseSandboxEgressCredentialToken as Wt, EVENT_WAIT_MS as X, createUserTokenStore as Xt, ingestEvent as Y, startOAuthFlow as Yt, isEventMailboxMetadata as Z, buildSteeringPiMessage as Zt, resolvePluginOAuthAccount as _, getConversationWorkState as _t, buildAuthPauseResponse as a, shouldEmitDevAgentTrace as an, beginConversationResume as at, unpublishedArtifactHeaders as b, recordAttemptFailure as bt, deleteMcpAuthSessionsForUserProvider as c, clearConsumedConversationWake as ct, getMcpAuthSession as d, completeConversationWork as dt, toolInvocationDestination as en, eventGuidance as et, getMcpStoredOAuthCredentials as f, countPendingConversationMessages as ft, readStats as g, getConversation as gt, latestProgressStatus as h, ensureConversationWake as ht, executeCredentialedEgressRequest as i, runDispatchOutcome as in, appendAndEnqueueInboundMessage as it, resolveSlackChannelIdFromThreadId as j, hasRunnableConversationWork$1 as jt, readSlackRawMessageContext as k, CONVERSATION_WORK_STALE_ENQUEUE_MS as kt, deleteMcpServerSessionId as l, clearExpiredConversationLease as lt, PluginCredentialFailureError as m, drainConversationMailbox as mt, executeAgentRun as n, deliversFinalText as nn, ackMessages as nt, finalizeMcpAuthorization as o, getConfigDefaults as on, cancelHumanFacingPendingMessages as ot, putMcpStoredOAuthCredentials as p, deadLetterAttempt as pt, getMcpProviderErrorAttributes as q, parseOAuthStatePayload as qt, createContextCompactor as r, finishedRunReply as rn, appendAndEnqueueExclusiveInboundMessage as rt, deleteMcpAuthSession as s, setConfigDefaults as sn, checkInConversationWork as st, createAgentRunner as t, AuthorizationFlowDisabledError as tn, pluginEventCatalog as tt, deleteMcpStoredOAuthCredentials as u, completeConversationStop as ut, publicArtifactHeaders as v, hasRunnableConversationWork as vt, collectAttachmentGarbage as w, requestAnotherSlice as wt, lookupSlackUserProfile as x, recordConversationRetry as xt, readPublicArtifact as y, listActiveConversationIds as yt, eventAutomationTriggerAvailable as z, shouldPropagateSandboxEgressTrace as zt };
|