@sentry/junior 0.271.0 → 0.273.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/{adapter-Bqbtr1RV.js → adapter-umap1cth.js} +1 -1
- package/dist/{admin-CWBjzyN0.js → admin-0o6E_3lo.js} +1 -1
- package/dist/{agent-runner-BiHsH8O_.js → agent-runner-D7Qz-lGO.js} +899 -631
- package/dist/api/schema.d.ts +2 -2
- package/dist/api/schema.js +5 -5
- package/dist/api.d.ts +1 -1
- package/dist/api.js +62 -537
- package/dist/{app-C81xfpNa.d.ts → app-BcyhJq8x.d.ts} +4 -9
- package/dist/app.d.ts +1 -1
- package/dist/app.js +673 -134
- package/dist/{assistant-reply-DgtX9DaJ.js → assistant-reply-wZ7i4OFa.js} +1 -1
- package/dist/{automation-Dwh_xcqM.js → automation-yffXcZt9.js} +1 -1
- package/dist/{briefs-0FeSJHQ3.js → briefs-CgMnlpIp.js} +4 -4
- package/dist/{catalog-runtime-Bw9kjGVu.js → catalog-runtime-TygDsPOU.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +1 -1
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-B0jlQSgO.js → client-DhqbV0Fe.js} +63 -5
- package/dist/{reporting-schema-CZREhYc-.js → common-Eu6sx09c.js} +4 -1
- package/dist/{config-DrAeD8sr.js → config-C6FTd7PX.js} +7 -15
- package/dist/{connections-BzN2K0G9.js → connections-Dq72KajR.js} +11 -11
- package/dist/{conversation-AhOVzN_k.js → conversation-T4oabbBi.js} +22 -1
- package/dist/{conversation-privacy-D1NhUxyo.js → conversation-privacy-BeRgrsph.js} +1 -1
- package/dist/{credential-sync-DpNWl4yy.js → credential-sync-BmCbHAp4.js} +15 -16
- package/dist/{db-DSabI7jJ.d.ts → db-DOzBGLn5.d.ts} +11 -1
- package/dist/{executor-C2cRw0hZ.js → executor-BafgvQcc.js} +25 -7
- package/dist/instrumentation.js +114 -3
- package/dist/{job-queue-CEeLYkSj.js → job-queue-C4lphW9Y.js} +3 -3
- package/dist/{logging-DuwXUuIY.js → logging-CNs1e04n.js} +6 -1
- package/dist/message-CETC5lsl.js +186 -0
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-oM5TJt34.js → oauth-callback-server-DcQZsKMm.js} +2 -2
- package/dist/{credentialed-mh7HBqey.js → projection-BOpi891B.js} +4576 -49
- package/dist/{prompt-BPSdXYXV.js → prompt-B1ySjUyf.js} +1 -1
- package/dist/{registry-BPtbAVFo.js → registry-B-yUrv3h.js} +2 -2
- package/dist/{resolve-D44G1jJm.js → resolve-DqVtd4Q9.js} +3 -3
- package/dist/{result-CvP8c5bb.js → result-DQwTiLTw.js} +16 -12
- package/dist/{runner-FIGIKvk2.js → runner-Dco5qMq6.js} +11 -11
- package/dist/{sandbox-egress-signals-C_JzucwP.js → sandbox-egress-signals-DFc_9Mcv.js} +1 -1
- package/dist/{schema-DKZgO-xf.js → schema-5xHDGlBW.js} +10 -9
- package/dist/{schema-DPCvePop.d.ts → schema-9OGdsaxF.d.ts} +28 -3
- package/dist/{service-account-1mJGRAvx.js → service-account-CWfk9Cs-.js} +3 -3
- package/dist/{service-account-CHO3YXpt.js → service-account-smdPFWNy.js} +3 -3
- package/dist/{session-stop-DuUB1rwQ.js → session-stop-DaT_7tYa.js} +50 -24
- package/dist/{skills-DHK7L9Eo.js → skills-BbtJSF2-.js} +2 -2
- package/dist/{spawn-CKl_ySIq.js → spawn-Ceikdjkp.js} +2 -2
- package/dist/{list-DjJJ5K0x.js → stop-dKUc-x2K.js} +1017 -469
- package/dist/{store-Di0E4EvU.js → store-C1xuZcLd.js} +48 -31
- package/dist/{store-BF2XEeQz.js → store-DaxUuOmD.js} +3 -3
- package/dist/{task-queue-CC7IFtbT.js → task-queue-ymAh_-P1.js} +4 -4
- package/dist/{task-runner-DKZtMFwa.js → task-runner-vlH6pEm3.js} +6 -6
- package/dist/{thread-state-BQ1T3eeF.js → thread-state-BkUpE9Ae.js} +4 -5
- package/dist/{turn-failure-response-B5eSFoHn.js → turn-failure-response-DIsD7ip_.js} +7 -8
- package/dist/{turn-lifecycle-CTLEIEOn.js → turn-lifecycle-BdPKdPS8.js} +1 -1
- package/dist/turn-session-routing-BdCTDq9m.js +1625 -0
- package/dist/{validation-Cjo-3yt3.js → validation-BQjCfh84.js} +1 -1
- package/dist/{version-D30MBLLw.js → version-BpiFGZZW.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-D3jhuP66.js → viewer-C6Y1aXjZ.js} +49 -21
- package/dist/{work-CL9QwQcl.js → work-ChPh_YEh.js} +27 -39
- package/dist/{worker-S2P7IJDI.js → worker-Be_sK_ou.js} +6 -6
- package/migrations/0072_conversation_read_positions.sql +10 -0
- package/migrations/meta/0072_snapshot.json +4064 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/dist/message-CRe3WBKc.js +0 -59
- package/dist/turn-session-routing-BFVp8TbU.js +0 -6072
- package/migrations/AGENTS.md +0 -8
- package/dist/{oauth-relay-CTygMkNx.js → oauth-relay-C6ZAyJgf.js} +1 -1
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
|
|
2
|
-
import { $ as
|
|
3
|
-
import { _ as
|
|
4
|
-
import {
|
|
5
|
-
import { B as isRecord$4, C as summarizeMessageText, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, _ as runBestEffort, a as extractGenAiUsageSummary, b as setSpanAttributes, f as logError, g as normalizeGenAiFinishReason, h as logWarn, i as extractGenAiUsageAttributes, l as getLogContextAttributesForSpan, m as logInfo, p as logException, u as getTracePropagationHeaders, v as serializeGenAiAttribute, x as setSpanStatus, z as normalizeIdentityEmail } from "./logging-
|
|
2
|
+
import { $ as loadTurnCheckpoint, $n as canAgentChangeReviewItem, $r as isAlreadyExistsError, $t as isAssistantMessage, Ai as findMatchingWatches, An as getPluginSystemPromptContributions, Ar as formatProviderLabel, B as createPluginEgress, Ba as matchSlackAttachments, Bn as closeReviewItem, Br as addReactionToMessage, Bt as TurnSliceLimitExceededError, Ci as STOP_WATCHING_TOOL_NAME, Cn as getOAuthAccountProviders, Cr as hash, D as fallbackShortTitle, Di as createTimerWatch, En as createPluginHookRunner, Er as getSandboxProvider, G as AuthorizationFlowDisabledError, Ga as createPluginAnnotations, Gi as getSlackClient, Gn as insertReviewItem, Gt as CODEMODE_TOOL_NAME, H as createPluginAuthOrchestration, Hi as downloadPrivateSlackFile, Hn as dismissOpenReviewItems, Hr as getSlackMessagePermalink, Ir as normalizeSlackReplyMarkdown, J as canReusePendingAuthLink, Ja as defaultTimezone, Ji as normalizeSlackConversationId, Jn as searchReviewItems, Jr as updateSlackMessage, Jt as getPiMessageRole, K as AuthorizationPauseError, Ki as isConversationScopedChannel, Kr as postSlackMessage, Mi as listWatches, Mn as getPluginUserPromptContributions, Mr as resolveBaseUrl, Nn as getPlugins, O as resolveTaskTitle, Oi as createWatch, Pr as escapeSlackMrkdwnText, Q as continuableMessages, Qr as isAbortError, Rr as slackMention, Rt as TURN_LIMIT_WRAP_UP_MESSAGE, Si as RESOURCE_WATCH_TOOL_SOURCE, Sn as createUserTokenStore, Sr as create, Ti as cancelWatch, Tr as ensureDockerDaemon, U as authorizationId, Ui as getFilePermalink, Un as getOpenReviewItemByDedupeKey, Ur as openSlackDirectMessage, Ut as createTurnLimitGate, V as PluginCredentialFailureError, Vi as SlackActionError, Vr as deleteSlackMessage, Vt as TurnToolCallLimitExceededError, W as AuthPausePersistenceError, Wa as saveObjectAnnotations, Wn as getReviewItem, Wt as isTurnExecutionLimitExceededError, Xi as withSlackRetries, Xn as updateReviewItemContent, Xt as getUserMessageInstructionText, Ya as resolveUserTimezone, Yi as runWithSlackInstallationToken, Yr as uploadFilesToConversation, Yt as getTerminalAssistantMessages, Zi as sleep, Zn as PLUGIN_REVIEW_ITEM_KEY_PREFIX, Zt as hasRuntimeTurnContext, _a as sandboxSkillDir, _i as workspaceRepoCheckoutPath, _n as getAgentContextCompactionTriggerTokens, _r as WorkspaceValidationError, a as loadLastModelProfile, ai as workspaceSnapshotNotReadyUserMessage, an as stripRuntimeTurnContext, bi as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, bn as isExperimentalFeatureEnabled, br as setWorkspaceSnapshot, c as loadTurnRoute, ca as buildCommandScript, ci as consumeSandboxEgressPermissionDeniedSignal, cn as SESSION_CONTEXT_SECTION_TAG, cr as resolveAssignee, d as recordAttachmentsDelivered, da as SANDBOX_COMMAND_PATH, dn as buildDeterministicTurnId, dr as deleteWorkspace, ei as throwSandboxOperationError, en as isContinuablePiBoundary, eo as historyItemFromPiMessage, er as canReadReviewItem, et as saveTurnCheckpoint, fa as SANDBOX_DATA_ROOT, fi as buildSandboxEgressNetworkPolicy, fr as getWorkspace, g as recordMcpProviderConnected, ga as SANDBOX_WORKSPACE_ROOT, gi as tryWorkspaceRepoCheckoutPath, gn as estimateTextTokens, gr as updateWorkspace, h as recordGuardianActionReviewed, ha as SANDBOX_SKILLS_ROOT, hi as SANDBOX_RUNTIME, i as loadConversationProjection, ia as getDashboardTaskLink, ii as isWorkspaceSnapshotNotReadyError, in as retainRuntimeTurnContext, io as getConfigDefaults, ir as requireReviewItemLinks, ji as getWatch, jn as getPluginTools, ki as deliverWatch, kr as deliverOAuthAuthorization, l as openConversationProjection, la as buildNonInteractiveShellScript, li as createSandboxEgressCredentialToken, ln as TURN_CONTEXT_TAG, lr as resolveAssignees, m as recordAuthorizationRequested, ma as SANDBOX_REPOS_ROOT, mr as listWorkspaces, ni as WorkspaceSnapshotNotReadyError, nn as isToolResultMessage, no as JUNIOR_NATIVE_EVENT_NAMESPACE, nr as readReviewItemAgentScope, oa as ToolInputError, oi as clearSandboxEgressSignals, on as trimTrailingAssistantMessages, or as EVERYONE_ASSIGNEE, pa as SANDBOX_JUNIOR_ROOT, pi as resolveSandboxCommandEnvironment, pr as getWorkspaceByName, q as abandonReplacedPendingAuth, qt as extractAssistantText, r as loadConnectedMcpProviders, ra as getDashboardReviewItemLink, ri as getWorkspaceSnapshotNotReadyError, rn as normalizeToolNameFromResult, ro as conversationNoteEvent, rr as requireReviewItemAssignees, sa as PluginToolDeniedError, si as consumeSandboxEgressAuthRequiredSignal, sn as REQUEST_CONTEXT_SECTION_TAG, sr as automationCreatorUserId, ta as getDashboardConversationLink, ti as wrapSandboxSetupError, tn as isToolResultError, to as piMessageFromHistoryItem, u as recordAgentsInstructionsUpdated, ua as runNonInteractiveCommand, ur as createWorkspace, v as recordToolExecutionStarted, va as sandboxSkillFile, vi as resolveChannelCapabilities, vn as getAgentContextInputLimitTokens, vr as invalidateReadySnapshot, wi as cancelSubscriptions, wr as isStale, xi as RESOURCE_SUBSCRIPTION_MAX_TTL_MS, xr as setWorkspaceSnapshotBuild, y as recordTurnRoute, ya as sandboxSkillPathResolution, yi as getSlackToolContext, yr as loadSnapshotsForProfile, zt as TurnBudgetExceededError } from "./projection-BOpi891B.js";
|
|
3
|
+
import { S as resolveGatewayCredential, _ as findProviderError, a as completeText, b as isProviderRetryError, c as resolveGatewayModel, d as isCompactionSummaryText, f as renderCurrentInstruction, g as createProviderError, h as ProviderError, i as completeObject, l as COMPACTION_SUMMARY_PREFIX, m as escapeXml, n as GEN_AI_SERVER_ADDRESS, p as unwrapCurrentInstruction, t as GEN_AI_PROVIDER_NAME, u as hasCompactedConversationContext, v as getProviderErrorAttributes, x as getGatewayApiKey } from "./client-DhqbV0Fe.js";
|
|
4
|
+
import { _ as normalizeSlackEmojiName, n as botConfig } from "./config-C6FTd7PX.js";
|
|
5
|
+
import { B as isRecord$4, C as summarizeMessageText, E as withSpan, H as toOptionalString, S as setTags, T as withLogContext, _ as runBestEffort, a as extractGenAiUsageSummary, b as setSpanAttributes, f as logError, g as normalizeGenAiFinishReason, h as logWarn, i as extractGenAiUsageAttributes, l as getLogContextAttributesForSpan, m as logInfo, p as logException, u as getTracePropagationHeaders, v as serializeGenAiAttribute, x as setSpanStatus, z as normalizeIdentityEmail } from "./logging-CNs1e04n.js";
|
|
6
6
|
import { g as withActiveSpan, i as getActiveSpan, m as startInactiveSpan } from "./sentry-MDkYAVPV.js";
|
|
7
|
-
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-
|
|
7
|
+
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-BeRgrsph.js";
|
|
8
8
|
import { a as formatModelProfile, c as modelProfileSchema, l as profileConfig, n as ModelProfileNotConfiguredError, o as formatModelProfiles, r as boundProfile, s as modelIdForProfile, u as TURN_REASONING_LEVELS } from "./model-profile-B998sjVd.js";
|
|
9
9
|
import { d as parseSlackChannelReferenceId, f as parseSlackTeamId, h as parseSlackMessageTs, n as createSlackActor, o as isUserActor, p as parseSlackUserId, u as parseSlackChannelId } from "./actor-B4DYygOB.js";
|
|
10
|
-
import { C as getSqlExecutor,
|
|
10
|
+
import { $ as sameDestination, C as getSqlExecutor, G as agentTurnCostUsd, J as hasAgentTurnUsage, M as SYSTEM_IDENTITY_ID, N as resolveActorIdentityId, S as getDb, W as addAgentTurnUsage, Z as parseDestination, a as readActorIdentityFromSql, b as getConversationMessageSearchStore, f as identityAvatarUrl, h as readUserAvatarUrls, i as readActorIdentity, x as getConversationStore, y as getConversationEventStore } from "./viewer-C6Y1aXjZ.js";
|
|
11
11
|
import { a as juniorAgentInvocations, c as juniorIdentities, d as juniorDestinations, i as juniorAgentBindings, l as juniorUsers, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
|
|
12
|
-
import {
|
|
12
|
+
import { E as juniorArtifacts, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, d as juniorAutomations, f as juniorStats, l as juniorAutomationRuns, s as juniorReviewItemSlackCards } from "./executor-BafgvQcc.js";
|
|
13
13
|
import { _ as sameActorIdentity, g as instructionProvenanceFor, h as instructionActors, i as KNOWN_CONVERSATION_EVENT_TYPES, m as contextProvenance, n as detectImageMediaType, p as agentInstructionProvenance, r as isVisionImageMediaType } from "./media-U6Tuz6Mi.js";
|
|
14
14
|
import { a as listReferenceFiles, c as soulPathCandidates, l as worldPathCandidates } from "./package-discovery-DJBompdd.js";
|
|
15
|
-
import { c as fetchWithBoundedOAuthErrorBodies, d as actorLogContext, f as credentialContextForActor, g as credentialUserSubjectId, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, s as OAuthProviderError } from "./registry-
|
|
16
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
17
|
-
import { a as parseSkillInvocation, i as parseSkillFile, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-
|
|
18
|
-
import { $ as loadSnapshotsForProfile, $n as sandboxSkillDir, $t as workspaceRepoCheckoutPath, A as updateReviewItemContent, An as normalizeSlackConversationId, B as EVERYONE_ASSIGNEE, Bn as getDashboardTaskLink, Bt as clearSandboxEgressSignals, C as getOpenReviewItemByDedupeKey, Cn as SlackActionError, Ct as openSlackDirectMessage, Dn as getSlackClient, Et as postSlackMessage, Ft as wrapSandboxSetupError, G as deleteWorkspace, Gn as buildNonInteractiveShellScript, H as resolveAssignee, Hn as ToolInputError, Ht as consumeSandboxEgressPermissionDeniedSignal, I as readReviewItemAgentScope, Ir as conversationNoteEvent, It as WorkspaceSnapshotNotReadyError, J as listWorkspaces, Jn as SANDBOX_DATA_ROOT, Jt as resolveSandboxCommandEnvironment, K as getWorkspace, Kn as runNonInteractiveCommand, L as requireReviewItemAssignees, Ln as getDashboardConversationLink, Lt as getWorkspaceSnapshotNotReadyError, Mn as withSlackRetries, Mr as JUNIOR_NATIVE_EVENT_NAMESPACE, Mt as isAbortError, N as canAgentChangeReviewItem, Nn as sleep, Nt as isAlreadyExistsError, O as searchReviewItems, On as isConversationScopedChannel, Or as defaultTimezone, Ot as updateSlackMessage, P as canReadReviewItem, Pt as throwSandboxOperationError, Q as invalidateReadySnapshot, Qn as SANDBOX_WORKSPACE_ROOT, Qt as tryWorkspaceRepoCheckoutPath, R as requireReviewItemLinks, Rt as isWorkspaceSnapshotNotReadyError, S as dismissOpenReviewItems, St as getSlackMessagePermalink, T as insertReviewItem, Tn as getFilePermalink, Tr as createPluginAnnotations, U as resolveAssignees, Un as PluginToolDeniedError, Ut as createSandboxEgressCredentialToken, V as automationCreatorUserId, Vt as consumeSandboxEgressAuthRequiredSignal, W as createWorkspace, Wn as buildCommandScript, X as updateWorkspace, Xn as SANDBOX_REPOS_ROOT, Yn as SANDBOX_JUNIOR_ROOT, Z as WorkspaceValidationError, Zn as SANDBOX_SKILLS_ROOT, Zt as SANDBOX_RUNTIME, an as STOP_WATCHING_TOOL_NAME, at as ensureDockerDaemon, b as closeReviewItem, br as matchSlackAttachments, bt as addReactionToMessage, d as getPluginTools, dn as deliverWatch, en as resolveChannelCapabilities, er as sandboxSkillFile, et as setWorkspaceSnapshot, f as getPluginUserPromptContributions, fn as findMatchingWatches, ft as resolveBaseUrl, gt as normalizeSlackReplyMarkdown, in as RESOURCE_WATCH_TOOL_SOURCE, it as isStale, j as PLUGIN_REVIEW_ITEM_KEY_PREFIX, jn as runWithSlackInstallationToken, kr as resolveUserTimezone, kt as uploadFilesToConversation, ln as createTimerWatch, lt as deliverOAuthAuthorization, mn as listWatches, mt as escapeSlackMrkdwnText, n as createUserTokenStore, nn as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, nt as create, o as createPluginHookRunner, on as cancelSubscriptions, ot as getSandboxProvider, p as getPlugins, pn as getWatch, q as getWorkspaceByName, qn as SANDBOX_COMMAND_PATH, qt as buildSandboxEgressNetworkPolicy, r as getOAuthAccountProviders, rn as RESOURCE_SUBSCRIPTION_MAX_TTL_MS, rt as hash, sn as cancelWatch, tn as getSlackToolContext, tr as sandboxSkillPathResolution, tt as setWorkspaceSnapshotBuild, u as getPluginSystemPromptContributions, un as createWatch, ut as formatProviderLabel, vt as slackMention, w as getReviewItem, wn as downloadPrivateSlackFile, wr as saveObjectAnnotations, xt as deleteSlackMessage, zn as getDashboardReviewItemLink, zt as workspaceSnapshotNotReadyUserMessage } from "./credentialed-mh7HBqey.js";
|
|
15
|
+
import { c as fetchWithBoundedOAuthErrorBodies, d as actorLogContext, f as credentialContextForActor, g as credentialUserSubjectId, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, s as OAuthProviderError } from "./registry-B-yUrv3h.js";
|
|
16
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-TygDsPOU.js";
|
|
17
|
+
import { a as parseSkillInvocation, i as parseSkillFile, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-BbtJSF2-.js";
|
|
19
18
|
import { a as withLock, i as fenceLock } from "./locks-D-0h111u.js";
|
|
20
|
-
import { r as getStateAdapter } from "./adapter-
|
|
19
|
+
import { r as getStateAdapter } from "./adapter-umap1cth.js";
|
|
21
20
|
import { i as isValidTimeZone, r as getZonedDateTimeParts, t as getFirstRunAtMs } from "./cadence-R7Em4RjE.js";
|
|
22
|
-
import {
|
|
23
|
-
import { r as EVENT_AUTHOR_ID } from "./prompt-
|
|
24
|
-
import { A as
|
|
25
|
-
import { a as isNoReplyMarker, i as NO_REPLY_MARKER, n as getAssistantReplyText, r as sanitizeAssistantText, t as decideReply } from "./assistant-reply-
|
|
26
|
-
import {
|
|
21
|
+
import { J as briefOutcomeStatusSchema, V as removedCardSchema, Y as conversationBriefSchema, q as briefLinkSchema } from "./conversation-T4oabbBi.js";
|
|
22
|
+
import { r as EVENT_AUTHOR_ID } from "./prompt-B1ySjUyf.js";
|
|
23
|
+
import { A as AgentMessagePausedError, F as CHILD_AGENT_RESULTS, I as PARENT_AGENT_TARGET, J as isTurnInputCommitLostError, L as agentNameSchema, M as AgentMessageTargetError, N as AgentNameInUseError, R as AGENT_TYPES, U as CooperativeTurnYieldError, V as formatAgentTypes, W as TurnInputCommitLostError, _ as invocationFromRow, d as getAgentInvocationMessageId, g as getLatestAgentInvocationForConversation, h as getChildAgentLockName, j as AgentMessageQueueFullError, k as AgentInvocationLimitError, t as NON_TERMINAL_AGENT_INVOCATION_STATUSES, u as getAgentInvocationIdForResult, v as isTerminalAgentInvocation } from "./store-C1xuZcLd.js";
|
|
24
|
+
import { a as isNoReplyMarker, i as NO_REPLY_MARKER, n as getAssistantReplyText, r as sanitizeAssistantText, t as decideReply } from "./assistant-reply-wZ7i4OFa.js";
|
|
25
|
+
import { E as resolveSlackReplyTarget, L as readAutomationResult, M as FINISH_AUTOMATION_RUN_TOOL_NAME, N as automationResultSchema$1, R as remindMissingAutomationResult, _ as slackChannelMessageTrigger, a as EVENT_WAIT_MS, b as workspaceSnapshotFinishedEvent, d as getEventCatalog, n as markConversationsStopped, o as enqueueEventNotification, p as requireSlackChannelMessageScope, t as getConversationStoppedAtMs, v as WORKSPACE_SNAPSHOT_FAILED_EVENT, x as workspaceSnapshotWatch, y as WORKSPACE_SNAPSHOT_READY_EVENT } from "./session-stop-DaT_7tYa.js";
|
|
27
26
|
import { C as userCustomizationTextSchema, b as webMessageId, g as formatUserInputAnswer, h as buttonUserInputField, m as USER_INPUT_NOTE, p as USER_INPUT_LIMITS, s as sumUtcHoursIntoSixHours, v as normalizeUserInputAnswer, x as USER_CUSTOMIZATION_MAX_CHARS, y as userInputRequestSchema } from "./reporting-window-DgHgwQB3.js";
|
|
28
|
-
import { a as setSnapshotSpanAttributes, i as postinstall, n as resolve$1, r as dependencies, s as getSandboxResources } from "./resolve-
|
|
29
|
-
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-
|
|
30
|
-
import { D as stopConversationWork, n as appendAndEnqueueExclusiveInboundMessage, r as appendAndEnqueueInboundMessage } from "./store-
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
27
|
+
import { a as setSnapshotSpanAttributes, i as postinstall, n as resolve$1, r as dependencies, s as getSandboxResources } from "./resolve-DqVtd4Q9.js";
|
|
28
|
+
import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-C4lphW9Y.js";
|
|
29
|
+
import { D as stopConversationWork, n as appendAndEnqueueExclusiveInboundMessage, r as appendAndEnqueueInboundMessage } from "./store-DaxUuOmD.js";
|
|
30
|
+
import { $ as saveEventAutomation, A as readAutomationRuns, B as readScheduledAutomation, C as readAutomationExecutionByConversationId, Ct as requireEventIdentifier, D as readAutomationExecutionStatusHours, E as readAutomationExecutionStatusDays, I as createScheduledAutomation, J as deleteEventAutomation, L as deleteScheduledAutomation, O as readAutomationExecutionSummaries, Q as parseEventAutomationRow, St as registeredResourceTypeSchema, T as readAutomationExecutionHours, V as saveScheduledAutomation, X as getEventAutomation, Z as listDeletedEventAutomationsCreatedBy, _t as eventWatchUnavailable, at as listAutomationVersions, bt as registeredEventTriggerProblems, c as getDispatchIdForConversation, et as setEventAutomationStatus, ft as automationEventCatalog, gt as eventNamespaceSchema, k as readAutomationExecutions, l as getDispatchInputMessageId, lt as automationRevision, ot as readAutomationVersion, pt as describeRegisteredEventTrigger, q as createEventAutomation, rt as automationDefinition, st as sameDefinitionValue, t as resolveConversationRouting, u as getDispatchRecord, ut as requireAutomationRevision, vt as hasEventCatalogEntries, w as readAutomationExecutionDays, wt as requireValidEventTrigger, x as emptyAutomationRunWindows, xt as registeredEventTypeSchema, yt as registeredEventMatchSchema, z as parseScheduledAutomationRow } from "./turn-session-routing-BdCTDq9m.js";
|
|
31
|
+
import { _ as getMcpProviderErrorAttributes, a as serviceAccountOnly, c as createMcpServiceAccountClientProvider, d as PluginMcpClient, f as McpProviderError, g as getMcpAwareTelemetryMessage, h as getMcpAwareErrorType, i as serviceAccountAllows, l as getMcpServiceAccountCredentials, m as McpToolError, p as McpServiceAccountRequiredError, u as McpAuthorizationRequiredError, v as toMcpProviderError } from "./service-account-smdPFWNy.js";
|
|
32
|
+
import { A as SCHEDULED_AUTOMATION_SYSTEM_ACTOR, D as taskOutcomeInputSchema, E as automationTitleSchema, F as scheduleIntentSchema, I as weekdaySchema, N as ScheduleIntentError, P as compileScheduleIntent, T as automationInstructionToolSchema, k as eventAutomationTriggerSchema, l as automationListQuerySchema } from "./automation-yffXcZt9.js";
|
|
33
33
|
import { n as privateTraceResultAttributes } from "./private-trace-result-CE7_lEdq.js";
|
|
34
34
|
import { streamSimple } from "@earendil-works/pi-ai/compat";
|
|
35
35
|
import { createGatewayProvider } from "@ai-sdk/gateway";
|
|
36
|
-
import { generateText } from "ai";
|
|
36
|
+
import { NoImageGeneratedError, generateImage, generateText } from "ai";
|
|
37
37
|
import path from "node:path";
|
|
38
38
|
import { isDeepStrictEqual } from "node:util";
|
|
39
|
-
import { z } from "zod";
|
|
40
39
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
40
|
+
import { z } from "zod";
|
|
41
41
|
import { and, asc, count, desc, eq, exists, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists, notInArray, or, sql } from "drizzle-orm";
|
|
42
|
-
import { PluginToolInputError, createWebSource, eventMatchFieldSchema, eventReachSchema, eventSchema, eventTriggerText, localDestinationSchema, missingToolAnnotationKeys, ownedObjectAnnotationSchema, sourceSchema, stableEventMatchKey, watchResultSchema } from "@sentry/junior-plugin-api";
|
|
42
|
+
import { PluginToolInputError, createWebSource, eventMatchFieldSchema, eventMatchSchema, eventReachSchema, eventSchema, eventTriggerText, localDestinationSchema, missingToolAnnotationKeys, ownedObjectAnnotationSchema, sourceSchema, stableEventMatchKey, watchResultSchema } from "@sentry/junior-plugin-api";
|
|
43
43
|
import { alias } from "drizzle-orm/pg-core";
|
|
44
44
|
import { Agent, estimateContextTokens } from "@earendil-works/pi-agent-core";
|
|
45
45
|
import fs from "node:fs/promises";
|
|
@@ -48,6 +48,7 @@ import http from "node:http";
|
|
|
48
48
|
import dns from "node:dns/promises";
|
|
49
49
|
import net from "node:net";
|
|
50
50
|
import { SignJWT, importPKCS8 } from "jose";
|
|
51
|
+
import { performance as performance$1 } from "node:perf_hooks";
|
|
51
52
|
import { createAssistantMessageEventStream, isRetryableAssistantError } from "@earendil-works/pi-ai";
|
|
52
53
|
import { Bash, defineCommand } from "just-bash";
|
|
53
54
|
import { structuredPatch } from "diff";
|
|
@@ -202,172 +203,6 @@ var SkillSandbox = class {
|
|
|
202
203
|
throw new Error("No active skill is loaded. Call loadSkill first or pass skill_name explicitly.");
|
|
203
204
|
}
|
|
204
205
|
};
|
|
205
|
-
// src/chat/tools/sandbox/file-utils.ts
|
|
206
|
-
const MAX_TEXT_CHARS = 6e4;
|
|
207
|
-
const RIPGREP_EXCLUDED_GLOBS = ["!**/.git/**", "!**/node_modules/**"];
|
|
208
|
-
const DEFAULT_SEARCH_COMMAND_TIMEOUT_MS = 3e4;
|
|
209
|
-
const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
210
|
-
/** Normalize model-supplied numeric knobs before they reach filesystem tools. */
|
|
211
|
-
function positiveInteger$1(value) {
|
|
212
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
213
|
-
const integer = Math.floor(value);
|
|
214
|
-
return integer > 0 ? integer : void 0;
|
|
215
|
-
}
|
|
216
|
-
/** Compare and slice text in one line-ending space while preserving callers' output rules. */
|
|
217
|
-
function normalizeToLf$1(value) {
|
|
218
|
-
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
219
|
-
}
|
|
220
|
-
/** Keep tool output inside the model-facing budget with an explicit notice. */
|
|
221
|
-
function truncateText$1(value, maxChars = MAX_TEXT_CHARS) {
|
|
222
|
-
if (value.length <= maxChars) return {
|
|
223
|
-
content: value,
|
|
224
|
-
truncated: false
|
|
225
|
-
};
|
|
226
|
-
const removed = value.length - maxChars;
|
|
227
|
-
return {
|
|
228
|
-
content: `${value.slice(0, maxChars)}\n\n[output truncated: ${removed} characters removed]`,
|
|
229
|
-
truncated: true
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
/** Detect model-facing missing paths without swallowing sandbox lifecycle/API failures. */
|
|
233
|
-
function isMissingPathError(error) {
|
|
234
|
-
if (!error || typeof error !== "object") return false;
|
|
235
|
-
const candidate = error;
|
|
236
|
-
if (candidate.code === "ENOENT") return true;
|
|
237
|
-
return typeof candidate.message === "string" && candidate.message.startsWith("File not found:");
|
|
238
|
-
}
|
|
239
|
-
/** Reject an expected missing search/list path through the tool-error channel. */
|
|
240
|
-
function missingPathSearchResult(params) {
|
|
241
|
-
const textPath = params.displayPath ?? params.missingPath ?? params.path;
|
|
242
|
-
throw new ToolInputError(`Path not found: ${textPath}`);
|
|
243
|
-
}
|
|
244
|
-
function escapeRegExp(value) {
|
|
245
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
246
|
-
}
|
|
247
|
-
function globToRegExp(pattern) {
|
|
248
|
-
let source = "";
|
|
249
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
250
|
-
const char = pattern.charAt(index);
|
|
251
|
-
const next = pattern[index + 1];
|
|
252
|
-
if (char === "*" && next === "*") {
|
|
253
|
-
if (pattern[index + 2] === "/") {
|
|
254
|
-
source += "(?:.*/)?";
|
|
255
|
-
index += 2;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
source += ".*";
|
|
259
|
-
index += 1;
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (char === "*") {
|
|
263
|
-
source += "[^/]*";
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
if (char === "?") {
|
|
267
|
-
source += "[^/]";
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
source += escapeRegExp(char);
|
|
271
|
-
}
|
|
272
|
-
return new RegExp(`^${source}$`);
|
|
273
|
-
}
|
|
274
|
-
function matchesGlob(relativePath, pattern) {
|
|
275
|
-
const matcher = globToRegExp(pattern);
|
|
276
|
-
if (matcher.test(relativePath)) return true;
|
|
277
|
-
if (pattern.startsWith("**/") && matchesGlob(relativePath, pattern.slice(3))) return true;
|
|
278
|
-
return !pattern.includes("/") && matcher.test(path.posix.basename(relativePath));
|
|
279
|
-
}
|
|
280
|
-
/** Keep sandbox filesystem tools scoped to the mounted workspace root. */
|
|
281
|
-
function resolveWorkspacePath(input, fallback = ".") {
|
|
282
|
-
const requested = (input ?? "").trim() || fallback;
|
|
283
|
-
const absolute = requested.startsWith("/") ? requested : path.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
|
|
284
|
-
const normalized = path.posix.normalize(absolute);
|
|
285
|
-
if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) throw new ToolInputError(`Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`);
|
|
286
|
-
return normalized;
|
|
287
|
-
}
|
|
288
|
-
/** Anchor ripgrep paths and globs to the requested search root. */
|
|
289
|
-
function getRipgrepSearchLocation(root, rootIsDirectory) {
|
|
290
|
-
if (rootIsDirectory) return {
|
|
291
|
-
cwd: root,
|
|
292
|
-
target: "."
|
|
293
|
-
};
|
|
294
|
-
return {
|
|
295
|
-
cwd: path.posix.dirname(root),
|
|
296
|
-
target: path.posix.basename(root)
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/** Share bounded workspace traversal across search tools so their skip rules stay aligned. */
|
|
300
|
-
async function collectFiles(params) {
|
|
301
|
-
const files = [];
|
|
302
|
-
let limitReached = false;
|
|
303
|
-
let missingPath;
|
|
304
|
-
const visit = async (dirPath) => {
|
|
305
|
-
if (missingPath) return;
|
|
306
|
-
let entries;
|
|
307
|
-
try {
|
|
308
|
-
entries = (await params.fs.readdir(dirPath)).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
309
|
-
} catch (error) {
|
|
310
|
-
if (isMissingPathError(error)) {
|
|
311
|
-
missingPath = dirPath;
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
throw error;
|
|
315
|
-
}
|
|
316
|
-
for (const entry of entries) {
|
|
317
|
-
const fullPath = path.posix.join(dirPath, entry);
|
|
318
|
-
let stat;
|
|
319
|
-
try {
|
|
320
|
-
stat = await params.fs.stat(fullPath);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
if (isMissingPathError(error)) {
|
|
323
|
-
missingPath = fullPath;
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
if (stat.isDirectory()) {
|
|
329
|
-
if (!SKIPPED_DIRECTORIES.has(entry)) await visit(fullPath);
|
|
330
|
-
if (limitReached) return;
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
const relativePath = path.posix.relative(params.root, fullPath);
|
|
334
|
-
if (!params.pattern || matchesGlob(relativePath, params.pattern)) {
|
|
335
|
-
files.push(fullPath);
|
|
336
|
-
if (params.limit && files.length >= params.limit) {
|
|
337
|
-
limitReached = true;
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
};
|
|
343
|
-
let stat;
|
|
344
|
-
try {
|
|
345
|
-
stat = await params.fs.stat(params.root);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
if (isMissingPathError(error)) return {
|
|
348
|
-
files,
|
|
349
|
-
limitReached: false,
|
|
350
|
-
missingPath: params.root,
|
|
351
|
-
missingRoot: true
|
|
352
|
-
};
|
|
353
|
-
throw error;
|
|
354
|
-
}
|
|
355
|
-
if (!stat.isDirectory()) {
|
|
356
|
-
const relativePath = path.posix.basename(params.root);
|
|
357
|
-
return {
|
|
358
|
-
files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
|
|
359
|
-
limitReached: false,
|
|
360
|
-
missingRoot: false
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
await visit(params.root);
|
|
364
|
-
return {
|
|
365
|
-
files,
|
|
366
|
-
limitReached,
|
|
367
|
-
missingPath,
|
|
368
|
-
missingRoot: missingPath === params.root
|
|
369
|
-
};
|
|
370
|
-
}
|
|
371
206
|
// src/chat/repository-instructions.ts
|
|
372
207
|
const AGENTS_FILENAME = "AGENTS.md";
|
|
373
208
|
const MAX_REPOSITORY_INSTRUCTIONS_BYTES = 32768;
|
|
@@ -382,81 +217,207 @@ const REPOSITORY_SKILLS_HEADER = [
|
|
|
382
217
|
].join("\n");
|
|
383
218
|
const AGENTS_REPLACEMENT_NOTICE = "These AGENTS.md instructions replace all previously provided AGENTS.md instructions.";
|
|
384
219
|
const AGENTS_REMOVAL_NOTICE = "The previously provided AGENTS.md instructions no longer apply.";
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Read limit for each AGENTS.md and SKILL.md file. It is 3 bytes past the
|
|
222
|
+
* budget: a UTF-8 character has at most 4 bytes, so a character cut at this
|
|
223
|
+
* limit starts after the budget, and the budgeted text is the same as with a
|
|
224
|
+
* full read.
|
|
225
|
+
*/
|
|
226
|
+
const FILE_READ_LIMIT_BYTES = 32771;
|
|
227
|
+
/**
|
|
228
|
+
* Print every path and file that the instructions read needs, one record per
|
|
229
|
+
* line. The cwd is the workspace root, and all paths are relative to it.
|
|
230
|
+
*
|
|
231
|
+
* Arguments: the file read limit, then `repos` to read every Git worktree in
|
|
232
|
+
* it or "", then one group for each selected directory. A group holds the
|
|
233
|
+
* directories from the selected directory up to the workspace root, then "".
|
|
234
|
+
* A group stops at its first directory with `.git`.
|
|
235
|
+
*
|
|
236
|
+
* Records: `R <dir>` for a Git worktree in repos/, `G <dir>/.git`, `D <path>`
|
|
237
|
+
* for a directory in a skill root, `E <path>` for another entry in a skill
|
|
238
|
+
* root, and `F <base64> <path>` for an AGENTS.md or SKILL.md file. File
|
|
239
|
+
* tests follow symlinks like `stat -L`. Globs skip dot entries like `ls -1`,
|
|
240
|
+
* which the Vercel Sandbox `readdir` runs.
|
|
241
|
+
*/
|
|
242
|
+
const READ_SCRIPT = `
|
|
243
|
+
limit=$1
|
|
244
|
+
repos=$2
|
|
245
|
+
shift 2
|
|
246
|
+
|
|
247
|
+
nl='
|
|
248
|
+
'
|
|
249
|
+
# A glob result is an entry when it exists. Like the lines of \`ls -1\`, a
|
|
250
|
+
# name with a line break does not name an entry.
|
|
251
|
+
listed() {
|
|
252
|
+
case $1 in *"$nl"*) return 1 ;; esac
|
|
253
|
+
[ -e "$1" ] || [ -L "$1" ]
|
|
393
254
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
255
|
+
|
|
256
|
+
emit_file() {
|
|
257
|
+
[ -f "$1" ] || return 0
|
|
258
|
+
if [ ! -r "$1" ]; then
|
|
259
|
+
printf 'cannot read %s\\n' "$1" >&2
|
|
260
|
+
exit 1
|
|
261
|
+
fi
|
|
262
|
+
printf 'F '
|
|
263
|
+
head -c "$limit" < "$1" | base64 | tr -d '\\n'
|
|
264
|
+
printf ' %s\\n' "$1"
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
emit_skills() {
|
|
268
|
+
for entry in "$1/.agents/skills"/* "$1/.claude/skills"/*; do
|
|
269
|
+
listed "$entry" || continue
|
|
270
|
+
# The first skill root wins for each name. Skip entries that it shadows.
|
|
271
|
+
case $entry in
|
|
272
|
+
"$1/.claude/skills/"*) listed "$1/.agents/skills/\${entry##*/}" && continue ;;
|
|
273
|
+
esac
|
|
274
|
+
if [ -d "$entry" ]; then
|
|
275
|
+
printf 'D %s\\n' "$entry"
|
|
276
|
+
emit_file "$entry/SKILL.md"
|
|
277
|
+
else
|
|
278
|
+
printf 'E %s\\n' "$entry"
|
|
279
|
+
fi
|
|
280
|
+
done
|
|
401
281
|
}
|
|
402
|
-
|
|
282
|
+
|
|
283
|
+
if [ -n "$repos" ]; then
|
|
284
|
+
for repo in "$repos"/*; do
|
|
285
|
+
if listed "$repo" && [ -d "$repo" ] && [ -e "$repo/.git" ]; then
|
|
286
|
+
printf 'R %s\\n' "$repo"
|
|
287
|
+
set -- "$@" "$repo" ""
|
|
288
|
+
fi
|
|
289
|
+
done
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
found=
|
|
293
|
+
for dir in "$@"; do
|
|
294
|
+
if [ -z "$dir" ]; then
|
|
295
|
+
found=
|
|
296
|
+
elif [ -z "$found" ]; then
|
|
297
|
+
emit_file "$dir/AGENTS.md"
|
|
298
|
+
if [ -e "$dir/.git" ]; then
|
|
299
|
+
printf 'G %s/.git\\n' "$dir"
|
|
300
|
+
emit_skills "$dir"
|
|
301
|
+
found=1
|
|
302
|
+
fi
|
|
303
|
+
fi
|
|
304
|
+
done
|
|
305
|
+
`;
|
|
306
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
307
|
+
function compareStrings(left, right) {
|
|
308
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
309
|
+
}
|
|
310
|
+
/** Directories that the Git root search checks, from cwd up to the workspace root. */
|
|
311
|
+
function gitRootCandidates(cwd) {
|
|
312
|
+
const candidates = [];
|
|
403
313
|
let current = cwd;
|
|
404
314
|
for (;;) {
|
|
405
|
-
|
|
406
|
-
if (current === SANDBOX_WORKSPACE_ROOT) return;
|
|
315
|
+
candidates.push(current);
|
|
316
|
+
if (current === SANDBOX_WORKSPACE_ROOT) return candidates;
|
|
407
317
|
const parent = path.posix.dirname(current);
|
|
408
|
-
if (parent === current || parent !== SANDBOX_WORKSPACE_ROOT && !parent.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) return;
|
|
318
|
+
if (parent === current || parent !== SANDBOX_WORKSPACE_ROOT && !parent.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) return candidates;
|
|
409
319
|
current = parent;
|
|
410
320
|
}
|
|
411
321
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
322
|
+
function findGitRoot(tree, cwd) {
|
|
323
|
+
return gitRootCandidates(cwd).find((directory) => tree.gitMarkers.has(path.posix.join(directory, ".git")));
|
|
324
|
+
}
|
|
325
|
+
function invalidOutput() {
|
|
326
|
+
return /* @__PURE__ */ new Error("Repository instructions read printed invalid output");
|
|
327
|
+
}
|
|
328
|
+
function parseRepositoryTree(stdout) {
|
|
329
|
+
const tree = {
|
|
330
|
+
entries: /* @__PURE__ */ new Map(),
|
|
331
|
+
files: /* @__PURE__ */ new Map(),
|
|
332
|
+
gitMarkers: /* @__PURE__ */ new Set(),
|
|
333
|
+
repositories: []
|
|
334
|
+
};
|
|
335
|
+
const lines = stdout.split("\n");
|
|
336
|
+
if (lines.pop() !== "") throw invalidOutput();
|
|
337
|
+
for (const line of lines) {
|
|
338
|
+
const kind = line.slice(0, 2);
|
|
339
|
+
let target = line.slice(2);
|
|
340
|
+
let content = "";
|
|
341
|
+
if (kind === "F ") {
|
|
342
|
+
const separator = target.indexOf(" ");
|
|
343
|
+
if (separator < 0) throw invalidOutput();
|
|
344
|
+
content = target.slice(0, separator);
|
|
345
|
+
target = target.slice(separator + 1);
|
|
346
|
+
if (!BASE64_PATTERN.test(content)) throw invalidOutput();
|
|
347
|
+
}
|
|
348
|
+
if (!target) throw invalidOutput();
|
|
349
|
+
const absolute = path.posix.join(SANDBOX_WORKSPACE_ROOT, target);
|
|
350
|
+
switch (kind) {
|
|
351
|
+
case "R ":
|
|
352
|
+
tree.repositories.push(absolute);
|
|
353
|
+
break;
|
|
354
|
+
case "G ":
|
|
355
|
+
tree.gitMarkers.add(absolute);
|
|
356
|
+
break;
|
|
357
|
+
case "D ":
|
|
358
|
+
case "E ": {
|
|
359
|
+
const root = path.posix.dirname(absolute);
|
|
360
|
+
const entries = tree.entries.get(root) ?? /* @__PURE__ */ new Map();
|
|
361
|
+
entries.set(path.posix.basename(absolute), kind === "D ");
|
|
362
|
+
tree.entries.set(root, entries);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "F ":
|
|
366
|
+
tree.files.set(absolute, Buffer.from(content, "base64").toString("utf8"));
|
|
367
|
+
break;
|
|
368
|
+
default: throw invalidOutput();
|
|
369
|
+
}
|
|
420
370
|
}
|
|
421
|
-
|
|
371
|
+
tree.repositories.sort(compareStrings);
|
|
372
|
+
return tree;
|
|
422
373
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
374
|
+
/** Read what the selected directories need with one sandbox command. */
|
|
375
|
+
async function readRepositoryTree(args) {
|
|
376
|
+
const relative = (target) => path.posix.relative(SANDBOX_WORKSPACE_ROOT, target) || ".";
|
|
377
|
+
const result = await args.runCommand({
|
|
378
|
+
cmd: "sh",
|
|
379
|
+
args: [
|
|
380
|
+
"-c",
|
|
381
|
+
READ_SCRIPT,
|
|
382
|
+
"sh",
|
|
383
|
+
String(FILE_READ_LIMIT_BYTES),
|
|
384
|
+
args.discover ? relative(SANDBOX_REPOS_ROOT) : "",
|
|
385
|
+
...args.directories.flatMap((directory) => [...gitRootCandidates(directory).map(relative), ""])
|
|
386
|
+
],
|
|
387
|
+
cwd: SANDBOX_WORKSPACE_ROOT
|
|
388
|
+
});
|
|
389
|
+
if (result.exitCode !== 0) throw new Error(`Repository instructions read failed with exit code ${result.exitCode}: ${result.stderr.trim()}`);
|
|
390
|
+
return parseRepositoryTree(result.stdout);
|
|
431
391
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
} catch (error) {
|
|
436
|
-
if (isMissingPathError(error)) return [];
|
|
437
|
-
throw error;
|
|
438
|
-
}
|
|
392
|
+
function readInstructionFile(tree, filePath) {
|
|
393
|
+
const content = tree.files.get(filePath);
|
|
394
|
+
return content?.trim() ? content : void 0;
|
|
439
395
|
}
|
|
440
396
|
/** Read model-invocable skills from the repository skill directories. */
|
|
441
|
-
|
|
442
|
-
const roots = REPOSITORY_SKILL_ROOTS.map((root) => path.posix.join(gitRoot, root));
|
|
443
|
-
const listings = await Promise.all(roots.map(async (root) => await readOptionalDirectory(fs, root)));
|
|
397
|
+
function readRepositorySkills(tree, gitRoot) {
|
|
444
398
|
const candidates = /* @__PURE__ */ new Map();
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
399
|
+
for (const root of REPOSITORY_SKILL_ROOTS) {
|
|
400
|
+
const rootPath = path.posix.join(gitRoot, root);
|
|
401
|
+
const entries = [...tree.entries.get(rootPath) ?? []].sort(([left], [right]) => compareStrings(left, right));
|
|
402
|
+
for (const [name, directory] of entries) if (!candidates.has(name)) candidates.set(name, {
|
|
403
|
+
directory,
|
|
404
|
+
path: path.posix.join(rootPath, name)
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const skills = [];
|
|
408
|
+
for (const [name, candidate] of candidates) {
|
|
409
|
+
if (!candidate.directory) continue;
|
|
410
|
+
const filePath = path.posix.join(candidate.path, "SKILL.md");
|
|
411
|
+
const raw = readInstructionFile(tree, filePath);
|
|
452
412
|
const parsed = raw ? parseSkillFile(raw, name) : void 0;
|
|
453
|
-
if (!parsed?.ok || parsed.skill.disableModelInvocation)
|
|
454
|
-
|
|
413
|
+
if (!parsed?.ok || parsed.skill.disableModelInvocation) continue;
|
|
414
|
+
skills.push({
|
|
455
415
|
description: parsed.skill.description.replace(/\s+/g, " ").trim(),
|
|
456
416
|
name,
|
|
457
417
|
path: filePath
|
|
458
|
-
};
|
|
459
|
-
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return skills.sort((left, right) => compareStrings(left.name, right.name));
|
|
460
421
|
}
|
|
461
422
|
function sourceBytes(sources) {
|
|
462
423
|
return sources.reduce((total, source) => total + Buffer.byteLength(source.content, "utf8"), 0);
|
|
@@ -465,7 +426,7 @@ function skillListBytes(skills) {
|
|
|
465
426
|
return skills.reduce((total, skill) => total + repositorySkillBytes(skill), 0);
|
|
466
427
|
}
|
|
467
428
|
/** Read root-to-cwd AGENTS.md files within the byte budget. */
|
|
468
|
-
|
|
429
|
+
function readAgentsSources(args) {
|
|
469
430
|
const directories = [];
|
|
470
431
|
let current = args.cwd;
|
|
471
432
|
for (;;) {
|
|
@@ -479,7 +440,7 @@ async function readAgentsSources(args) {
|
|
|
479
440
|
for (const directory of directories) {
|
|
480
441
|
if (remaining <= 0) break;
|
|
481
442
|
const filePath = path.posix.join(directory, AGENTS_FILENAME);
|
|
482
|
-
const content =
|
|
443
|
+
const content = readInstructionFile(args.tree, filePath);
|
|
483
444
|
if (!content) continue;
|
|
484
445
|
const bytes = Buffer.from(content, "utf8");
|
|
485
446
|
const bounded = bytes.length <= remaining ? content : bytes.subarray(0, remaining).toString("utf8");
|
|
@@ -492,11 +453,11 @@ async function readAgentsSources(args) {
|
|
|
492
453
|
return sources;
|
|
493
454
|
}
|
|
494
455
|
/** Read repository skills that fit the byte budget, keeping each entry whole. */
|
|
495
|
-
|
|
456
|
+
function readRepositorySkillsWithinBudget(args) {
|
|
496
457
|
const skills = [];
|
|
497
458
|
let remaining = args.maxBytes;
|
|
498
459
|
if (remaining <= 0) return skills;
|
|
499
|
-
for (const skill of
|
|
460
|
+
for (const skill of readRepositorySkills(args.tree, args.gitRoot)) {
|
|
500
461
|
const bytes = repositorySkillBytes(skill);
|
|
501
462
|
if (bytes > remaining) break;
|
|
502
463
|
skills.push(skill);
|
|
@@ -556,19 +517,33 @@ function mergeRepositoryInstructions(bundles) {
|
|
|
556
517
|
text
|
|
557
518
|
};
|
|
558
519
|
}
|
|
559
|
-
/**
|
|
560
|
-
|
|
561
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Read AGENTS.md instructions and repository skills for the selected
|
|
522
|
+
* directories with one sandbox command. Without directories, use every Git
|
|
523
|
+
* worktree under repos/.
|
|
524
|
+
*/
|
|
525
|
+
async function readRepositoryInstructions(args) {
|
|
526
|
+
const selected = [...new Set((args.directories ?? []).filter((directory) => directory === SANDBOX_WORKSPACE_ROOT || directory.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)))].sort(compareStrings);
|
|
527
|
+
if (args.directories && selected.length === 0) return {
|
|
528
|
+
directories: args.directories,
|
|
529
|
+
instructions: void 0
|
|
530
|
+
};
|
|
531
|
+
const tree = await readRepositoryTree({
|
|
532
|
+
directories: selected,
|
|
533
|
+
discover: !args.directories,
|
|
534
|
+
runCommand: args.runCommand
|
|
535
|
+
});
|
|
536
|
+
const cwds = args.directories ? selected : tree.repositories;
|
|
562
537
|
let remaining = MAX_REPOSITORY_INSTRUCTIONS_BYTES;
|
|
563
538
|
const repositories = [];
|
|
564
|
-
for (const directory of
|
|
565
|
-
const gitRoot =
|
|
539
|
+
for (const directory of cwds) {
|
|
540
|
+
const gitRoot = findGitRoot(tree, directory);
|
|
566
541
|
if (!gitRoot) continue;
|
|
567
|
-
const sources =
|
|
542
|
+
const sources = readAgentsSources({
|
|
568
543
|
cwd: directory,
|
|
569
|
-
fs: args.fs,
|
|
570
544
|
gitRoot,
|
|
571
|
-
maxBytes: remaining
|
|
545
|
+
maxBytes: remaining,
|
|
546
|
+
tree
|
|
572
547
|
});
|
|
573
548
|
remaining -= sourceBytes(sources);
|
|
574
549
|
repositories.push({
|
|
@@ -579,16 +554,19 @@ async function resolveRepositoryInstructionsForDirectories(args) {
|
|
|
579
554
|
}
|
|
580
555
|
const bundles = [];
|
|
581
556
|
for (const repository of repositories) {
|
|
582
|
-
const skills =
|
|
583
|
-
fs: args.fs,
|
|
557
|
+
const skills = readRepositorySkillsWithinBudget({
|
|
584
558
|
gitRoot: repository.gitRoot,
|
|
585
|
-
maxBytes: remaining
|
|
559
|
+
maxBytes: remaining,
|
|
560
|
+
tree
|
|
586
561
|
});
|
|
587
562
|
remaining -= skillListBytes(skills);
|
|
588
563
|
const bundle = buildRepositoryInstructions(repository.directory, repository.sources, skills);
|
|
589
564
|
if (bundle) bundles.push(bundle);
|
|
590
565
|
}
|
|
591
|
-
return
|
|
566
|
+
return {
|
|
567
|
+
directories: args.directories ?? cwds,
|
|
568
|
+
instructions: mergeRepositoryInstructions(bundles)
|
|
569
|
+
};
|
|
592
570
|
}
|
|
593
571
|
/** Render repository instructions with Codex's exact model-visible wrapper. */
|
|
594
572
|
function renderAgentsInstructions(args) {
|
|
@@ -725,6 +703,7 @@ function buildChatEndAttributes(message, mode) {
|
|
|
725
703
|
Object.assign(attributes, extractGenAiUsageAttributes(message));
|
|
726
704
|
if (message.stopReason) attributes["gen_ai.response.finish_reasons"] = [normalizeGenAiFinishReason(message.stopReason)];
|
|
727
705
|
if (message.model) attributes["gen_ai.response.model"] = message.model;
|
|
706
|
+
if (message.responseId) attributes["gen_ai.response.id"] = message.responseId;
|
|
728
707
|
if (message.stopReason === "error") {
|
|
729
708
|
const providerError = createProviderError(message.errorMessage ?? "");
|
|
730
709
|
Object.assign(attributes, getProviderErrorAttributes(providerError));
|
|
@@ -761,6 +740,22 @@ function createResponseTiming(requestStartedAtMs) {
|
|
|
761
740
|
}
|
|
762
741
|
};
|
|
763
742
|
}
|
|
743
|
+
function traceResponseHeaders(span, options) {
|
|
744
|
+
const eventLoopStart = performance$1.eventLoopUtilization();
|
|
745
|
+
const onResponse = options?.onResponse;
|
|
746
|
+
return {
|
|
747
|
+
...options,
|
|
748
|
+
onResponse: async (response, model) => {
|
|
749
|
+
try {
|
|
750
|
+
const vercelId = response.headers["x-vercel-id"];
|
|
751
|
+
if (vercelId) span.setAttribute("app.ai.gateway.vercel_id", vercelId);
|
|
752
|
+
const { utilization } = performance$1.eventLoopUtilization(eventLoopStart);
|
|
753
|
+
span.setAttribute("app.ai.event_loop.utilization", Math.round(utilization * 1e3) / 1e3);
|
|
754
|
+
} catch {}
|
|
755
|
+
await onResponse?.(response, model);
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
764
759
|
function relayStream(source, onEvent) {
|
|
765
760
|
const relayed = createAssistantMessageEventStream();
|
|
766
761
|
const finished = (async () => {
|
|
@@ -801,7 +796,7 @@ function createTracedStreamFn(baseOrOptions = streamSimple) {
|
|
|
801
796
|
});
|
|
802
797
|
try {
|
|
803
798
|
const requestStartedAtMs = Date.now();
|
|
804
|
-
const stream = await withActiveSpan(span, () => Promise.resolve(base(model, context, options)));
|
|
799
|
+
const stream = await withActiveSpan(span, () => Promise.resolve(base(model, context, traceResponseHeaders(span, options))));
|
|
805
800
|
const responseTiming = createResponseTiming(requestStartedAtMs);
|
|
806
801
|
const relay = relayStream(stream, responseTiming.observe);
|
|
807
802
|
relay.finished.then(() => stream.result()).then((finalMessage) => {
|
|
@@ -847,7 +842,7 @@ function shouldEmitDevAgentTrace() {
|
|
|
847
842
|
return process.env.NODE_ENV === "development";
|
|
848
843
|
}
|
|
849
844
|
// src/chat/agent-invocations/blocked-report.ts
|
|
850
|
-
/** Blocked report of a child agent.
|
|
845
|
+
/** Blocked report of a child agent. */
|
|
851
846
|
/** Model-facing tool that ends a child agent run as blocked. */
|
|
852
847
|
const REPORT_BLOCKED_TOOL_NAME = "reportBlocked";
|
|
853
848
|
const blockedReportSchema = z.object({ missing: z.string().trim().min(1) });
|
|
@@ -2217,8 +2212,8 @@ function buildTurnContextPrompt(params) {
|
|
|
2217
2212
|
/**
|
|
2218
2213
|
* Conversation notes: short facts that core attaches to a Conversation without
|
|
2219
2214
|
* starting a Turn. The next Turn shows them to the model in its runtime turn
|
|
2220
|
-
* context. A failed Turn also becomes a note for the next Turn.
|
|
2221
|
-
*
|
|
2215
|
+
* context. A failed Turn also becomes a note for the next Turn. A note is
|
|
2216
|
+
* context, never user intent, and only core adds one.
|
|
2222
2217
|
*/
|
|
2223
2218
|
/**
|
|
2224
2219
|
* Tell the next Turn that a Turn failed. Pi drops a failed assistant message
|
|
@@ -3961,9 +3956,10 @@ function createSandboxRuntime(options) {
|
|
|
3961
3956
|
withSpan: withSandboxSpan
|
|
3962
3957
|
});
|
|
3963
3958
|
};
|
|
3959
|
+
const ensureDocker = async (targetSandbox) => await withSandboxSpan("sandbox.docker.ensure", "sandbox.docker.ensure", {}, async () => await ensureDockerDaemon(targetSandbox));
|
|
3964
3960
|
const prepareSandbox = async (targetSandbox) => {
|
|
3965
3961
|
await syncSkills(targetSandbox);
|
|
3966
|
-
await
|
|
3962
|
+
await ensureDocker(targetSandbox);
|
|
3967
3963
|
await options.onSandboxPrepare?.(targetSandbox);
|
|
3968
3964
|
};
|
|
3969
3965
|
const applyNetworkPolicy = async (targetSandbox, traceHeaders) => {
|
|
@@ -4187,10 +4183,17 @@ function createSandboxRuntime(options) {
|
|
|
4187
4183
|
}
|
|
4188
4184
|
throw new Error("sandbox restore failed", { cause: error });
|
|
4189
4185
|
}
|
|
4190
|
-
let networkPolicyKey;
|
|
4191
4186
|
try {
|
|
4192
|
-
|
|
4193
|
-
|
|
4187
|
+
const steps = await Promise.allSettled([
|
|
4188
|
+
applyNetworkPolicy(hintedSandbox),
|
|
4189
|
+
syncSkills(hintedSandbox),
|
|
4190
|
+
ensureDocker(hintedSandbox)
|
|
4191
|
+
]);
|
|
4192
|
+
const failures = steps.flatMap((step) => step.status === "rejected" ? [step.reason] : []);
|
|
4193
|
+
if (failures.length > 0) throw failures.find((error) => getSandboxProvider().errors.isUnavailable(error)) ?? failures[0];
|
|
4194
|
+
const [policy] = steps;
|
|
4195
|
+
const networkPolicyKey = policy.status === "fulfilled" ? policy.value : void 0;
|
|
4196
|
+
await options.onSandboxPrepare?.(hintedSandbox);
|
|
4194
4197
|
dependencyProfileHash = ref.profileHash ?? dependencyProfileHash;
|
|
4195
4198
|
await persistSandboxRef({
|
|
4196
4199
|
...ref,
|
|
@@ -4212,17 +4215,26 @@ function createSandboxRuntime(options) {
|
|
|
4212
4215
|
const cachedSandbox = await tryReuseCachedSandbox();
|
|
4213
4216
|
if (cachedSandbox) {
|
|
4214
4217
|
setSpanAttributes({ "app.sandbox.acquire.outcome": "memory" });
|
|
4215
|
-
return
|
|
4218
|
+
return {
|
|
4219
|
+
session: cachedSandbox,
|
|
4220
|
+
fresh: false
|
|
4221
|
+
};
|
|
4216
4222
|
}
|
|
4217
4223
|
signal?.throwIfAborted();
|
|
4218
4224
|
const hintedSandbox = await tryRestoreHintedSandbox(signal);
|
|
4219
4225
|
if (hintedSandbox) {
|
|
4220
4226
|
setSpanAttributes({ "app.sandbox.acquire.outcome": "resumed" });
|
|
4221
|
-
return
|
|
4227
|
+
return {
|
|
4228
|
+
session: hintedSandbox,
|
|
4229
|
+
fresh: true
|
|
4230
|
+
};
|
|
4222
4231
|
}
|
|
4223
4232
|
const createdSandbox = await createFreshSandbox(signal);
|
|
4224
4233
|
setSpanAttributes({ "app.sandbox.acquire.outcome": "created" });
|
|
4225
|
-
return
|
|
4234
|
+
return {
|
|
4235
|
+
session: createdSandbox,
|
|
4236
|
+
fresh: true
|
|
4237
|
+
};
|
|
4226
4238
|
});
|
|
4227
4239
|
};
|
|
4228
4240
|
const getOrAcquireSandbox = async (signal) => {
|
|
@@ -4378,10 +4390,12 @@ function createSandboxRuntime(options) {
|
|
|
4378
4390
|
};
|
|
4379
4391
|
};
|
|
4380
4392
|
const ensureReadySandbox = async (signal) => {
|
|
4381
|
-
const session = await getOrAcquireSandbox(signal);
|
|
4382
|
-
signal?.throwIfAborted();
|
|
4383
|
-
await probeSession(session);
|
|
4393
|
+
const { session, fresh } = await getOrAcquireSandbox(signal);
|
|
4384
4394
|
signal?.throwIfAborted();
|
|
4395
|
+
if (!fresh) {
|
|
4396
|
+
await probeSession(session);
|
|
4397
|
+
signal?.throwIfAborted();
|
|
4398
|
+
}
|
|
4385
4399
|
await extendKeepAlive(session);
|
|
4386
4400
|
startKeepAlive(session);
|
|
4387
4401
|
return session;
|
|
@@ -4410,42 +4424,208 @@ function createSandboxRuntime(options) {
|
|
|
4410
4424
|
} catch (error) {
|
|
4411
4425
|
await stopSession(candidate?.session);
|
|
4412
4426
|
throw error;
|
|
4413
|
-
} finally {
|
|
4414
|
-
signal?.removeEventListener("abort", stopSwitching);
|
|
4427
|
+
} finally {
|
|
4428
|
+
signal?.removeEventListener("abort", stopSwitching);
|
|
4429
|
+
}
|
|
4430
|
+
const previous = activeSandbox;
|
|
4431
|
+
if (keepAliveTimer) {
|
|
4432
|
+
clearTimeout(keepAliveTimer);
|
|
4433
|
+
keepAliveTimer = void 0;
|
|
4434
|
+
}
|
|
4435
|
+
activeWorkspace = candidate.workspace;
|
|
4436
|
+
dependencyProfileHash = candidate.profileHash;
|
|
4437
|
+
activeSandbox = {
|
|
4438
|
+
session: candidate.session,
|
|
4439
|
+
networkPolicyKey: candidate.networkPolicyKey
|
|
4440
|
+
};
|
|
4441
|
+
startKeepAlive(candidate.session);
|
|
4442
|
+
await stopSession(previous?.session);
|
|
4443
|
+
},
|
|
4444
|
+
async acquire(signal) {
|
|
4445
|
+
return (await getOrAcquireSandbox(signal)).session;
|
|
4446
|
+
},
|
|
4447
|
+
async tools(signal) {
|
|
4448
|
+
return createToolExecutors(await ensureReadySandbox(signal));
|
|
4449
|
+
},
|
|
4450
|
+
async refreshNetworkPolicy(traceHeaders) {
|
|
4451
|
+
const active = activeSandbox;
|
|
4452
|
+
if (!active) return;
|
|
4453
|
+
await applyNetworkPolicy(active.session, traceHeaders);
|
|
4454
|
+
},
|
|
4455
|
+
close() {
|
|
4456
|
+
closed = true;
|
|
4457
|
+
if (keepAliveTimer) {
|
|
4458
|
+
clearTimeout(keepAliveTimer);
|
|
4459
|
+
keepAliveTimer = void 0;
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
};
|
|
4463
|
+
}
|
|
4464
|
+
// src/chat/tools/sandbox/file-utils.ts
|
|
4465
|
+
const MAX_TEXT_CHARS = 6e4;
|
|
4466
|
+
const RIPGREP_EXCLUDED_GLOBS = ["!**/.git/**", "!**/node_modules/**"];
|
|
4467
|
+
const DEFAULT_SEARCH_COMMAND_TIMEOUT_MS = 3e4;
|
|
4468
|
+
const SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
4469
|
+
/** Normalize model-supplied numeric knobs before they reach filesystem tools. */
|
|
4470
|
+
function positiveInteger$1(value) {
|
|
4471
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
4472
|
+
const integer = Math.floor(value);
|
|
4473
|
+
return integer > 0 ? integer : void 0;
|
|
4474
|
+
}
|
|
4475
|
+
/** Compare and slice text in one line-ending space while preserving callers' output rules. */
|
|
4476
|
+
function normalizeToLf$1(value) {
|
|
4477
|
+
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4478
|
+
}
|
|
4479
|
+
/** Keep tool output inside the model-facing budget with an explicit notice. */
|
|
4480
|
+
function truncateText$1(value, maxChars = MAX_TEXT_CHARS) {
|
|
4481
|
+
if (value.length <= maxChars) return {
|
|
4482
|
+
content: value,
|
|
4483
|
+
truncated: false
|
|
4484
|
+
};
|
|
4485
|
+
const removed = value.length - maxChars;
|
|
4486
|
+
return {
|
|
4487
|
+
content: `${value.slice(0, maxChars)}\n\n[output truncated: ${removed} characters removed]`,
|
|
4488
|
+
truncated: true
|
|
4489
|
+
};
|
|
4490
|
+
}
|
|
4491
|
+
/** Detect model-facing missing paths without swallowing sandbox lifecycle/API failures. */
|
|
4492
|
+
function isMissingPathError(error) {
|
|
4493
|
+
if (!error || typeof error !== "object") return false;
|
|
4494
|
+
const candidate = error;
|
|
4495
|
+
if (candidate.code === "ENOENT") return true;
|
|
4496
|
+
return typeof candidate.message === "string" && candidate.message.startsWith("File not found:");
|
|
4497
|
+
}
|
|
4498
|
+
/** Reject an expected missing search/list path through the tool-error channel. */
|
|
4499
|
+
function missingPathSearchResult(params) {
|
|
4500
|
+
const textPath = params.displayPath ?? params.missingPath ?? params.path;
|
|
4501
|
+
throw new ToolInputError(`Path not found: ${textPath}`);
|
|
4502
|
+
}
|
|
4503
|
+
function escapeRegExp(value) {
|
|
4504
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4505
|
+
}
|
|
4506
|
+
function globToRegExp(pattern) {
|
|
4507
|
+
let source = "";
|
|
4508
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
4509
|
+
const char = pattern.charAt(index);
|
|
4510
|
+
const next = pattern[index + 1];
|
|
4511
|
+
if (char === "*" && next === "*") {
|
|
4512
|
+
if (pattern[index + 2] === "/") {
|
|
4513
|
+
source += "(?:.*/)?";
|
|
4514
|
+
index += 2;
|
|
4515
|
+
continue;
|
|
4516
|
+
}
|
|
4517
|
+
source += ".*";
|
|
4518
|
+
index += 1;
|
|
4519
|
+
continue;
|
|
4520
|
+
}
|
|
4521
|
+
if (char === "*") {
|
|
4522
|
+
source += "[^/]*";
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
if (char === "?") {
|
|
4526
|
+
source += "[^/]";
|
|
4527
|
+
continue;
|
|
4528
|
+
}
|
|
4529
|
+
source += escapeRegExp(char);
|
|
4530
|
+
}
|
|
4531
|
+
return new RegExp(`^${source}$`);
|
|
4532
|
+
}
|
|
4533
|
+
function matchesGlob(relativePath, pattern) {
|
|
4534
|
+
const matcher = globToRegExp(pattern);
|
|
4535
|
+
if (matcher.test(relativePath)) return true;
|
|
4536
|
+
if (pattern.startsWith("**/") && matchesGlob(relativePath, pattern.slice(3))) return true;
|
|
4537
|
+
return !pattern.includes("/") && matcher.test(path.posix.basename(relativePath));
|
|
4538
|
+
}
|
|
4539
|
+
/** Keep sandbox filesystem tools scoped to the mounted workspace root. */
|
|
4540
|
+
function resolveWorkspacePath(input, fallback = ".") {
|
|
4541
|
+
const requested = (input ?? "").trim() || fallback;
|
|
4542
|
+
const absolute = requested.startsWith("/") ? requested : path.posix.join(SANDBOX_WORKSPACE_ROOT, requested);
|
|
4543
|
+
const normalized = path.posix.normalize(absolute);
|
|
4544
|
+
if (normalized !== SANDBOX_WORKSPACE_ROOT && !normalized.startsWith(`${SANDBOX_WORKSPACE_ROOT}/`)) throw new ToolInputError(`Path must stay within ${SANDBOX_WORKSPACE_ROOT}: ${requested}`);
|
|
4545
|
+
return normalized;
|
|
4546
|
+
}
|
|
4547
|
+
/** Anchor ripgrep paths and globs to the requested search root. */
|
|
4548
|
+
function getRipgrepSearchLocation(root, rootIsDirectory) {
|
|
4549
|
+
if (rootIsDirectory) return {
|
|
4550
|
+
cwd: root,
|
|
4551
|
+
target: "."
|
|
4552
|
+
};
|
|
4553
|
+
return {
|
|
4554
|
+
cwd: path.posix.dirname(root),
|
|
4555
|
+
target: path.posix.basename(root)
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
/** Share bounded workspace traversal across search tools so their skip rules stay aligned. */
|
|
4559
|
+
async function collectFiles(params) {
|
|
4560
|
+
const files = [];
|
|
4561
|
+
let limitReached = false;
|
|
4562
|
+
let missingPath;
|
|
4563
|
+
const visit = async (dirPath) => {
|
|
4564
|
+
if (missingPath) return;
|
|
4565
|
+
let entries;
|
|
4566
|
+
try {
|
|
4567
|
+
entries = (await params.fs.readdir(dirPath)).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
4568
|
+
} catch (error) {
|
|
4569
|
+
if (isMissingPathError(error)) {
|
|
4570
|
+
missingPath = dirPath;
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
throw error;
|
|
4574
|
+
}
|
|
4575
|
+
for (const entry of entries) {
|
|
4576
|
+
const fullPath = path.posix.join(dirPath, entry);
|
|
4577
|
+
let stat;
|
|
4578
|
+
try {
|
|
4579
|
+
stat = await params.fs.stat(fullPath);
|
|
4580
|
+
} catch (error) {
|
|
4581
|
+
if (isMissingPathError(error)) {
|
|
4582
|
+
missingPath = fullPath;
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
throw error;
|
|
4415
4586
|
}
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4587
|
+
if (stat.isDirectory()) {
|
|
4588
|
+
if (!SKIPPED_DIRECTORIES.has(entry)) await visit(fullPath);
|
|
4589
|
+
if (limitReached) return;
|
|
4590
|
+
continue;
|
|
4420
4591
|
}
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
await stopSession(previous?.session);
|
|
4429
|
-
},
|
|
4430
|
-
async acquire(signal) {
|
|
4431
|
-
return await getOrAcquireSandbox(signal);
|
|
4432
|
-
},
|
|
4433
|
-
async tools(signal) {
|
|
4434
|
-
return createToolExecutors(await ensureReadySandbox(signal));
|
|
4435
|
-
},
|
|
4436
|
-
async refreshNetworkPolicy(traceHeaders) {
|
|
4437
|
-
const active = activeSandbox;
|
|
4438
|
-
if (!active) return;
|
|
4439
|
-
await applyNetworkPolicy(active.session, traceHeaders);
|
|
4440
|
-
},
|
|
4441
|
-
close() {
|
|
4442
|
-
closed = true;
|
|
4443
|
-
if (keepAliveTimer) {
|
|
4444
|
-
clearTimeout(keepAliveTimer);
|
|
4445
|
-
keepAliveTimer = void 0;
|
|
4592
|
+
const relativePath = path.posix.relative(params.root, fullPath);
|
|
4593
|
+
if (!params.pattern || matchesGlob(relativePath, params.pattern)) {
|
|
4594
|
+
files.push(fullPath);
|
|
4595
|
+
if (params.limit && files.length >= params.limit) {
|
|
4596
|
+
limitReached = true;
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4446
4599
|
}
|
|
4447
4600
|
}
|
|
4448
4601
|
};
|
|
4602
|
+
let stat;
|
|
4603
|
+
try {
|
|
4604
|
+
stat = await params.fs.stat(params.root);
|
|
4605
|
+
} catch (error) {
|
|
4606
|
+
if (isMissingPathError(error)) return {
|
|
4607
|
+
files,
|
|
4608
|
+
limitReached: false,
|
|
4609
|
+
missingPath: params.root,
|
|
4610
|
+
missingRoot: true
|
|
4611
|
+
};
|
|
4612
|
+
throw error;
|
|
4613
|
+
}
|
|
4614
|
+
if (!stat.isDirectory()) {
|
|
4615
|
+
const relativePath = path.posix.basename(params.root);
|
|
4616
|
+
return {
|
|
4617
|
+
files: !params.pattern || matchesGlob(relativePath, params.pattern) ? [params.root] : [],
|
|
4618
|
+
limitReached: false,
|
|
4619
|
+
missingRoot: false
|
|
4620
|
+
};
|
|
4621
|
+
}
|
|
4622
|
+
await visit(params.root);
|
|
4623
|
+
return {
|
|
4624
|
+
files,
|
|
4625
|
+
limitReached,
|
|
4626
|
+
missingPath,
|
|
4627
|
+
missingRoot: missingPath === params.root
|
|
4628
|
+
};
|
|
4449
4629
|
}
|
|
4450
4630
|
// src/chat/tools/sandbox/text-edits.ts
|
|
4451
4631
|
const MAX_DIFF_LINE_CHARS = 4e3;
|
|
@@ -5706,13 +5886,12 @@ function createSandbox(options) {
|
|
|
5706
5886
|
return {
|
|
5707
5887
|
async captureRepositoryInstructions() {
|
|
5708
5888
|
if (!runtime.sandboxRef()) return;
|
|
5709
|
-
const {
|
|
5889
|
+
const { runCommand } = await runtime.tools();
|
|
5710
5890
|
const workspaceDirectories = activeWorkspace?.repos.map((repo) => tryWorkspaceRepoCheckoutPath(repo.repo)).filter((checkoutPath) => Boolean(checkoutPath)).map((checkoutPath) => `${SANDBOX_WORKSPACE_ROOT}/${checkoutPath}`) ?? [];
|
|
5711
5891
|
return await withSandboxSpan("sandbox.repository_instructions.read", "sandbox.repository_instructions.read", {}, async (setSpanAttributes) => {
|
|
5712
|
-
const directories
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
fs
|
|
5892
|
+
const { directories, instructions } = await readRepositoryInstructions({
|
|
5893
|
+
...workspaceDirectories.length > 0 ? { directories: workspaceDirectories } : void 0,
|
|
5894
|
+
runCommand
|
|
5716
5895
|
});
|
|
5717
5896
|
setSpanAttributes({
|
|
5718
5897
|
"app.repository_instructions.repo_count": directories.length,
|
|
@@ -6647,7 +6826,6 @@ async function enrichImagePrompt(rawPrompt) {
|
|
|
6647
6826
|
async function shrinkImage(image, maxBytes) {
|
|
6648
6827
|
const { default: sharp } = await import("sharp");
|
|
6649
6828
|
const { hasAlpha } = await sharp(image.data).metadata();
|
|
6650
|
-
const mimeType = hasAlpha ? "image/webp" : "image/jpeg";
|
|
6651
6829
|
for (const edge of RESIZE_LONGEST_EDGES) {
|
|
6652
6830
|
const resized = sharp(image.data).rotate().resize({
|
|
6653
6831
|
width: edge,
|
|
@@ -6659,8 +6837,7 @@ async function shrinkImage(image, maxBytes) {
|
|
|
6659
6837
|
if (data.byteLength <= maxBytes) return {
|
|
6660
6838
|
image: {
|
|
6661
6839
|
path: image.path,
|
|
6662
|
-
data
|
|
6663
|
-
mimeType
|
|
6840
|
+
data
|
|
6664
6841
|
},
|
|
6665
6842
|
width: info.width,
|
|
6666
6843
|
height: info.height
|
|
@@ -6710,17 +6887,6 @@ function extensionForMediaType$1(mediaType) {
|
|
|
6710
6887
|
if (mediaType === "image/gif") return "gif";
|
|
6711
6888
|
return "bin";
|
|
6712
6889
|
}
|
|
6713
|
-
function parseImageGenerationError(status, body, model) {
|
|
6714
|
-
if (!body) return `image generation failed: ${status}`;
|
|
6715
|
-
try {
|
|
6716
|
-
const message = JSON.parse(body).error?.message?.trim();
|
|
6717
|
-
if (!message) return `image generation failed: ${status} ${body}`;
|
|
6718
|
-
if (message.includes("not an image model")) return `image generation failed: configured model "${model}" is not an image generation model. Set AI_IMAGE_MODEL to a compatible image model.`;
|
|
6719
|
-
return `image generation failed: ${status} ${message}`;
|
|
6720
|
-
} catch {
|
|
6721
|
-
return `image generation failed: ${status} ${body}`;
|
|
6722
|
-
}
|
|
6723
|
-
}
|
|
6724
6890
|
/** Create the image tool with sandbox artifact guidance matched to file-send capability. */
|
|
6725
6891
|
function createImageGenerateTool(hooks, options, deps = {}) {
|
|
6726
6892
|
return zodTool({
|
|
@@ -6737,8 +6903,10 @@ function createImageGenerateTool(hooks, options, deps = {}) {
|
|
|
6737
6903
|
}),
|
|
6738
6904
|
outputSchema: imageGenerateOutputSchema,
|
|
6739
6905
|
execute: async ({ prompt, images = [] }) => {
|
|
6740
|
-
const
|
|
6741
|
-
|
|
6906
|
+
const gateway = createGatewayProvider({
|
|
6907
|
+
apiKey: await getGatewayApiKey(),
|
|
6908
|
+
...deps.fetch ? { fetch: deps.fetch } : void 0
|
|
6909
|
+
});
|
|
6742
6910
|
const model = options.modelId;
|
|
6743
6911
|
const { images: inputImages, resized } = await fitImagesToRequest(await Promise.all(images.map(async (path) => ({
|
|
6744
6912
|
path,
|
|
@@ -6749,58 +6917,26 @@ function createImageGenerateTool(hooks, options, deps = {}) {
|
|
|
6749
6917
|
resize_notice: "Some input images were too large for the image model, so they were resized first. Tell the user which images were resized."
|
|
6750
6918
|
} : {};
|
|
6751
6919
|
const enrichedPrompt = inputImages.length > 0 ? prompt : await enrichImagePrompt(prompt);
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
}
|
|
6765
|
-
|
|
6766
|
-
model,
|
|
6767
|
-
messages: [{
|
|
6768
|
-
role: "user",
|
|
6769
|
-
content
|
|
6770
|
-
}],
|
|
6771
|
-
modalities: ["image"]
|
|
6772
|
-
})
|
|
6773
|
-
});
|
|
6774
|
-
if (!response.ok) {
|
|
6775
|
-
const text = await response.text();
|
|
6776
|
-
throw new Error(parseImageGenerationError(response.status, text, model));
|
|
6777
|
-
}
|
|
6778
|
-
const payload = await response.json();
|
|
6779
|
-
const uploads = [];
|
|
6780
|
-
const generatedImages = payload.choices?.[0]?.message?.images ?? [];
|
|
6781
|
-
for (const [index, image] of generatedImages.entries()) {
|
|
6782
|
-
let bytes = null;
|
|
6783
|
-
let mimeType = "image/png";
|
|
6784
|
-
const url = image.image_url?.url;
|
|
6785
|
-
if (typeof url === "string" && url.startsWith("data:")) {
|
|
6786
|
-
const match = url.match(/^data:([^;,]+);base64,(.+)$/);
|
|
6787
|
-
if (!match) continue;
|
|
6788
|
-
mimeType = match[1] ?? mimeType;
|
|
6789
|
-
bytes = Buffer.from(match[2] ?? "", "base64");
|
|
6790
|
-
} else if (typeof url === "string" && url.length > 0) {
|
|
6791
|
-
const fetched = await fetchImpl(url);
|
|
6792
|
-
if (!fetched.ok) continue;
|
|
6793
|
-
mimeType = fetched.headers.get("content-type") ?? mimeType;
|
|
6794
|
-
bytes = Buffer.from(await fetched.arrayBuffer());
|
|
6795
|
-
}
|
|
6796
|
-
if (!bytes) continue;
|
|
6797
|
-
const extension = extensionForMediaType$1(mimeType);
|
|
6798
|
-
uploads.push({
|
|
6799
|
-
data: bytes,
|
|
6800
|
-
filename: `generated-image-${Date.now()}-${index + 1}.${extension}`,
|
|
6801
|
-
mimeType
|
|
6802
|
-
});
|
|
6920
|
+
let generatedImages;
|
|
6921
|
+
try {
|
|
6922
|
+
({images: generatedImages} = await generateImage({
|
|
6923
|
+
model: gateway.imageModel(model),
|
|
6924
|
+
prompt: inputImages.length > 0 ? {
|
|
6925
|
+
text: enrichedPrompt,
|
|
6926
|
+
images: inputImages.map((image) => image.data)
|
|
6927
|
+
} : enrichedPrompt,
|
|
6928
|
+
n: 1
|
|
6929
|
+
}));
|
|
6930
|
+
} catch (error) {
|
|
6931
|
+
if (NoImageGeneratedError.isInstance(error)) generatedImages = [];
|
|
6932
|
+
else if (error instanceof Error && error.message.includes("not an image model")) throw new Error(`image generation failed: configured model "${model}" is not an image generation model. Set AI_IMAGE_MODEL to an AI Gateway image model.`);
|
|
6933
|
+
else throw error;
|
|
6803
6934
|
}
|
|
6935
|
+
const uploads = generatedImages.map((image, index) => ({
|
|
6936
|
+
data: Buffer.from(image.uint8Array),
|
|
6937
|
+
filename: `generated-image-${Date.now()}-${index + 1}.${extensionForMediaType$1(image.mediaType)}`,
|
|
6938
|
+
mimeType: image.mediaType
|
|
6939
|
+
}));
|
|
6804
6940
|
if (uploads.length > 0) {
|
|
6805
6941
|
const artifactRefs = await hooks.writeGeneratedArtifacts(uploads);
|
|
6806
6942
|
return {
|
|
@@ -7448,10 +7584,13 @@ const messageAgentOutputSchema = juniorToolOutputSchema.extend({
|
|
|
7448
7584
|
conversation_id: z.string().min(1).describe("Target conversation ID"),
|
|
7449
7585
|
delivery: z.enum(["started", "queued"]).describe("started: the child agent starts work on the message now. queued: the target reads the message at its next step or after its current work.")
|
|
7450
7586
|
});
|
|
7451
|
-
/**
|
|
7587
|
+
/**
|
|
7588
|
+
* Create the tool that sends one message to the parent, a child agent, or
|
|
7589
|
+
* another top-level agent.
|
|
7590
|
+
*/
|
|
7452
7591
|
function createMessageAgentTool(messageAgent) {
|
|
7453
7592
|
return zodTool({
|
|
7454
|
-
description: `Send a message to your parent agent or to a child agent that this conversation started. Use it to give an existing child agent more work, an answer, or a correction, and to send your parent agent a finding or a question while you keep working. A message to a child continues its history. If the child is still working, it reads the message at its next step or after its current work, and one result covers both. The result arrives later as a new task in this conversation; do not wait or poll for it. A message to your parent does not end your task. Use spawnAgent only to start a new child agent. Your text is not user approval. A child's action review reads the user's own messages in the root conversation.`,
|
|
7593
|
+
description: `Send a message to your parent agent or to a child agent that this conversation started. Use it to give an existing child agent more work, an answer, or a correction, and to send your parent agent a finding or a question while you keep working. A message to a child continues its history. If the child is still working, it reads the message at its next step or after its current work, and one result covers both. The result arrives later as a new task in this conversation; do not wait or poll for it. A message to your parent does not end your task. A top-level agent (its conversation is not a child agent, such as a Slack thread) can also message another top-level agent by its conversation_id, such as one from slackThreadRead. Both conversations must be public, or both private. A private one also needs the person in this Turn to be a member. The target runs the message as its own task and sends no result. Use spawnAgent only to start a new child agent. Your text is not user approval. A child's action review reads the user's own messages in the root conversation.`,
|
|
7455
7594
|
exposure: "modelOnly",
|
|
7456
7595
|
annotations: {
|
|
7457
7596
|
destructiveHint: false,
|
|
@@ -7460,7 +7599,7 @@ function createMessageAgentTool(messageAgent) {
|
|
|
7460
7599
|
readOnlyHint: false
|
|
7461
7600
|
},
|
|
7462
7601
|
inputSchema: z.object({
|
|
7463
|
-
to: z.string().trim().min(1).describe(`"${PARENT_AGENT_TARGET}" for your parent agent,
|
|
7602
|
+
to: z.string().trim().min(1).describe(`"${PARENT_AGENT_TARGET}" for your parent agent, the name or conversation_id of a child agent from spawnAgent or listAgents, or the conversation_id of another top-level agent`),
|
|
7464
7603
|
text: z.string().trim().min(1).max(MESSAGE_AGENT_TEXT_MAX_LENGTH).describe("Message text. Include all context the target needs.")
|
|
7465
7604
|
}).strict(),
|
|
7466
7605
|
outputSchema: messageAgentOutputSchema,
|
|
@@ -7477,6 +7616,7 @@ function createMessageAgentTool(messageAgent) {
|
|
|
7477
7616
|
});
|
|
7478
7617
|
} catch (error) {
|
|
7479
7618
|
if (error instanceof AgentMessageTargetError) throw new ToolInputError(`${error.message}. Use listAgents to find a child agent.`, { cause: error });
|
|
7619
|
+
if (error instanceof AgentMessagePausedError) throw new ToolInputError(`${error.message}. Do not send it more messages. Tell the user instead.`, { cause: error });
|
|
7480
7620
|
if (error instanceof AgentMessageQueueFullError) throw new ToolInputError(`${error.message}. Wait for its result, then send one message with everything it needs.`, { cause: error });
|
|
7481
7621
|
if (error instanceof AgentInvocationLimitError) throw new ToolInputError(`${error.message}. Wait for active children to finish before you give more work.`, { cause: error });
|
|
7482
7622
|
throw error;
|
|
@@ -8390,7 +8530,6 @@ function renderReviewItemMessage(item, change) {
|
|
|
8390
8530
|
"",
|
|
8391
8531
|
item.request && item.answer ? formatUserInputAnswer(item.request, item.answer) : "(no answer)"
|
|
8392
8532
|
].join("\n");
|
|
8393
|
-
case "completed": return [`${personName$1(change.user)} marked ${label} done.`, ...question].join("\n");
|
|
8394
8533
|
case "dismissed": return [`${personName$1(change.user)} dismissed ${label}.`, ...question].join("\n");
|
|
8395
8534
|
case "taken": return [`${personName$1(change.user)} took ${label}. They are now its only assignee.`, ...question].join("\n");
|
|
8396
8535
|
case "replied": return [
|
|
@@ -9019,7 +9158,7 @@ function createSearchEventTypesTool(catalog) {
|
|
|
9019
9158
|
openWorldHint: false,
|
|
9020
9159
|
readOnlyHint: true
|
|
9021
9160
|
},
|
|
9022
|
-
description: "Search the event types currently enabled without creating anything. Use only when the user asks what events are supported or the required resource type or event name is unclear. Includes plugin resources and core Workspace snapshot events. namespaces gives the reach of each event.
|
|
9161
|
+
description: "Search the event types currently enabled without creating anything. Use only when the user asks what events are supported or the required resource type or event name is unclear. Includes plugin resources and core Workspace snapshot events. namespaces gives the reach of each event. An event automation selects its events with match filters from the matchFields of its namespace. With reach \"all\", match can be left out to run for every resource. With { anyOf }, match must set at least one of those filters. With reach \"resource\", only watchEvents can use the event. Use watchEvents for temporary updates in the current conversation; use createAutomation with an event trigger for durable automation. This tool does not watch a resource, create an automation, or list concrete resources.",
|
|
9023
9162
|
inputSchema: z.object({
|
|
9024
9163
|
query: z.string().nullable().describe("Optional terms matching a namespace, resource type, or event name. Resource types that match more terms come first. Empty lists all enabled event types.").optional(),
|
|
9025
9164
|
namespace: eventNamespaceSchema(catalog).nullable().describe("Optional enabled plugin namespace to search within.").optional(),
|
|
@@ -9461,11 +9600,16 @@ function requireCredentialStart(automation, isCreator) {
|
|
|
9461
9600
|
if (isCreator || automation.credentialMode !== "creator") return;
|
|
9462
9601
|
throw new AutomationEditError(`This ${"schedule" in automation ? "scheduled" : "event"} automation uses its creator's credentials. Only the creator can resume it or run it now. Switch it to system credentials first.`, "credentialMode");
|
|
9463
9602
|
}
|
|
9464
|
-
/**
|
|
9603
|
+
/**
|
|
9604
|
+
* Apply shared title, instruction, outcome, and creator credential rules.
|
|
9605
|
+
* Anyone who can manage the Automation can change any field except turning on
|
|
9606
|
+
* creator credentials. A non-creator change to execution or outcomes switches
|
|
9607
|
+
* it to system credentials, so creator credential output stays where the
|
|
9608
|
+
* creator approved.
|
|
9609
|
+
*/
|
|
9465
9610
|
async function editAutomationFields(current, input, isCreator, changesExecution) {
|
|
9466
9611
|
const kind = "schedule" in current ? "scheduled" : "event";
|
|
9467
9612
|
if (input.credentialMode === "creator" && !isCreator) throw new AutomationEditError(`Only the ${kind} automation creator can enable creator credential use.`, "credentialMode");
|
|
9468
|
-
if (input.outcomes !== void 0 && !isCreator) throw new AutomationEditError(`Only the ${kind} automation creator can change message destinations.`, "outcomes");
|
|
9469
9613
|
let outcomes = current.outcomes;
|
|
9470
9614
|
if (input.outcomes !== void 0) {
|
|
9471
9615
|
for (const outcome of input.outcomes) if (typeof outcome.destination !== "string" && !current.outcomes.some((stored) => isDeepStrictEqual(stored, outcome))) throw new AutomationEditError("Use the current Destination or the creator for a new message outcome.", "outcomes");
|
|
@@ -9479,10 +9623,11 @@ async function editAutomationFields(current, input, isCreator, changesExecution)
|
|
|
9479
9623
|
destination: outcome.destination
|
|
9480
9624
|
});
|
|
9481
9625
|
}
|
|
9626
|
+
const changesOutcomes = !isDeepStrictEqual(outcomes, current.outcomes);
|
|
9482
9627
|
return {
|
|
9483
9628
|
title: input.title ?? current.title,
|
|
9484
9629
|
task: { text: input.instruction ?? current.task.text },
|
|
9485
|
-
credentialMode: changesExecution && !isCreator ? "system" : input.credentialMode ?? current.credentialMode,
|
|
9630
|
+
credentialMode: (changesExecution || changesOutcomes) && !isCreator ? "system" : input.credentialMode ?? current.credentialMode,
|
|
9486
9631
|
outcomes
|
|
9487
9632
|
};
|
|
9488
9633
|
}
|
|
@@ -9535,14 +9680,32 @@ function registeredEventAutomationTriggerSchema(catalog) {
|
|
|
9535
9680
|
const plugins = automationEventCatalog(catalog);
|
|
9536
9681
|
return z.object({
|
|
9537
9682
|
namespace: eventNamespaceSchema(plugins),
|
|
9538
|
-
identifier: z.string().trim().min(1).max(300).nullable().describe("Exact resource identifier. Use null, with a null resourceType, to run for every resource of the namespace. Only events whose searchEventTypes reach is \"all\" or { anyOf } can run that way, and match must set one of the anyOf filters. When creating, omit it to run for every resource. When updating, omit it to keep the saved identifier.").optional(),
|
|
9539
|
-
resourceType: registeredResourceTypeSchema(plugins).nullable().describe("Resource type of the identifier. Use null when identifier is null. When updating, omit it to keep the saved resource type.").optional(),
|
|
9540
9683
|
label: z.string().trim().min(1).max(500),
|
|
9541
9684
|
events: z.array(registeredEventTypeSchema(plugins)).min(1),
|
|
9542
|
-
match: registeredEventMatchSchema()
|
|
9685
|
+
match: registeredEventMatchSchema().describe("Filters that select the events, from the matchFields of the namespace. Use the match of a subscribable tool result to select one resource, such as { repo, pullRequest } for one pull request. Leave it out to run for every resource, when the reach of each event allows that. A single value must equal the event data. A list means the event value may be any one entry. For a field with list: true, the event value is a list, and it matches when it includes the value or any entry.")
|
|
9543
9686
|
}).strict();
|
|
9544
9687
|
}
|
|
9545
9688
|
/**
|
|
9689
|
+
* Filter changes of an update. A value sets the filter, and null removes it.
|
|
9690
|
+
* A filter that is left out keeps its saved value, so an update cannot make
|
|
9691
|
+
* a trigger broader by leaving out a filter.
|
|
9692
|
+
*/
|
|
9693
|
+
const eventMatchChangesSchema = z.record(z.string(), z.union([eventMatchSchema.valueType, z.null()])).optional().describe("Filter changes, from the matchFields of the namespace. A value sets the filter, null removes it, and a filter that is left out keeps its saved value. When the namespace changes, the saved filters are dropped.");
|
|
9694
|
+
/** Build the event trigger that the update tool accepts. */
|
|
9695
|
+
function registeredEventAutomationTriggerChangesSchema(catalog) {
|
|
9696
|
+
return registeredEventAutomationTriggerSchema(catalog).extend({ match: eventMatchChangesSchema });
|
|
9697
|
+
}
|
|
9698
|
+
/**
|
|
9699
|
+
* Apply filter changes to the saved trigger, key by key. A change to another
|
|
9700
|
+
* namespace starts from no filters.
|
|
9701
|
+
*/
|
|
9702
|
+
function changedEventMatch(saved, namespace, changes) {
|
|
9703
|
+
const match = { ...namespace === saved.namespace ? saved.match : void 0 };
|
|
9704
|
+
for (const [key, value] of Object.entries(changes ?? {})) if (value === null) delete match[key];
|
|
9705
|
+
else match[key] = value;
|
|
9706
|
+
return Object.keys(match).length > 0 ? match : void 0;
|
|
9707
|
+
}
|
|
9708
|
+
/**
|
|
9546
9709
|
* Check a new event-automation trigger with the enabled plugins and return
|
|
9547
9710
|
* it as it is saved. It names a resource type and an identifier, or neither.
|
|
9548
9711
|
* The identifier gets its plugin's convention. Events and list values lose
|
|
@@ -9655,15 +9818,16 @@ function changesEventAutomationTrigger(current, next) {
|
|
|
9655
9818
|
async function editEventAutomation(current, input, isCreator, catalog) {
|
|
9656
9819
|
let trigger = current.trigger;
|
|
9657
9820
|
if (input.trigger !== void 0) {
|
|
9658
|
-
|
|
9659
|
-
|
|
9821
|
+
const inputTrigger = slackChannelMessageTrigger(input.trigger, current.destination.channelId);
|
|
9822
|
+
if (changesEventAutomationTrigger(current.trigger, inputTrigger) || current.trigger.resourceType !== inputTrigger.resourceType) try {
|
|
9823
|
+
trigger = requireEventAutomationTrigger(catalog, inputTrigger);
|
|
9660
9824
|
} catch (error) {
|
|
9661
9825
|
if (error instanceof ToolInputError) throw new AutomationEditError(error.message, "trigger");
|
|
9662
9826
|
throw error;
|
|
9663
9827
|
}
|
|
9664
9828
|
else trigger = {
|
|
9665
9829
|
...current.trigger,
|
|
9666
|
-
label:
|
|
9830
|
+
label: inputTrigger.label
|
|
9667
9831
|
};
|
|
9668
9832
|
}
|
|
9669
9833
|
const changesExecution = input.instruction !== void 0 && input.instruction !== current.task.text || changesEventAutomationTrigger(current.trigger, trigger);
|
|
@@ -10173,7 +10337,7 @@ async function readConversationSourceTask(args) {
|
|
|
10173
10337
|
// src/chat/automations/edit.ts
|
|
10174
10338
|
/**
|
|
10175
10339
|
* Web edits and lifecycle actions. Anyone who can read an Automation can
|
|
10176
|
-
* manage it. Creator
|
|
10340
|
+
* manage it. Creator credential rules still apply.
|
|
10177
10341
|
*/
|
|
10178
10342
|
/** Owners and public Destination readers can manage. Others get not found. */
|
|
10179
10343
|
async function requireEditableAutomation(user, id) {
|
|
@@ -10298,7 +10462,7 @@ async function restorableOutcomes(current, outcomes) {
|
|
|
10298
10462
|
});
|
|
10299
10463
|
continue;
|
|
10300
10464
|
}
|
|
10301
|
-
if (
|
|
10465
|
+
if (!task.outcomes.some((stored) => sameDefinitionValue(stored, outcome))) {
|
|
10302
10466
|
creatorDm ??= (await resolveTaskOutcomes([{
|
|
10303
10467
|
action: "send_message",
|
|
10304
10468
|
destination: "task_creator"
|
|
@@ -10778,15 +10942,23 @@ const automationSearchResultSchema = juniorToolOutputSchema.extend({
|
|
|
10778
10942
|
automations: z.array(automationResultSchema),
|
|
10779
10943
|
truncated: z.boolean()
|
|
10780
10944
|
}).strict();
|
|
10945
|
+
const scheduleTriggerSchema = z.object({
|
|
10946
|
+
type: z.literal("schedule"),
|
|
10947
|
+
schedule: scheduleIntentSchema.describe("When the automation runs. The scheduler computes the exact next run from this intent and the server clock.")
|
|
10948
|
+
}).strict();
|
|
10781
10949
|
/**
|
|
10782
|
-
* Build the trigger input of the create
|
|
10783
|
-
*
|
|
10950
|
+
* Build the trigger input of the create tool: a schedule, or the events of an
|
|
10951
|
+
* enabled plugin.
|
|
10784
10952
|
*/
|
|
10785
10953
|
function automationTriggerSchema(catalog) {
|
|
10786
|
-
return z.discriminatedUnion("type", [z.
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10954
|
+
return z.discriminatedUnion("type", [scheduleTriggerSchema, registeredEventAutomationTriggerSchema(catalog).extend({ type: z.literal("event") })]);
|
|
10955
|
+
}
|
|
10956
|
+
/**
|
|
10957
|
+
* Build the trigger input of the update tool. An event trigger changes its
|
|
10958
|
+
* filters key by key.
|
|
10959
|
+
*/
|
|
10960
|
+
function automationTriggerChangesSchema(catalog) {
|
|
10961
|
+
return z.discriminatedUnion("type", [scheduleTriggerSchema, registeredEventAutomationTriggerChangesSchema(catalog).extend({ type: z.literal("event") })]);
|
|
10790
10962
|
}
|
|
10791
10963
|
/** Return the Automation kind of a trigger input. */
|
|
10792
10964
|
function triggerKind(trigger) {
|
|
@@ -10980,7 +11152,7 @@ function createAutomationTool(context, catalog) {
|
|
|
10980
11152
|
creator: createdBy,
|
|
10981
11153
|
destinationChannelId: destination.channelId
|
|
10982
11154
|
});
|
|
10983
|
-
const eventTrigger = input.trigger.type === "event" ? requireEventAutomationTrigger(catalog, input.trigger) : void 0;
|
|
11155
|
+
const eventTrigger = input.trigger.type === "event" ? requireEventAutomationTrigger(catalog, slackChannelMessageTrigger(input.trigger, destination.channelId)) : void 0;
|
|
10984
11156
|
const toolCallId = options.toolCallId?.trim();
|
|
10985
11157
|
if (!toolCallId) throw new Error("Automation creation requires a tool-call identity.");
|
|
10986
11158
|
const id = automationIdForToolCall({
|
|
@@ -11045,205 +11217,75 @@ function createDeleteAutomationTool(context, catalog) {
|
|
|
11045
11217
|
}
|
|
11046
11218
|
// src/chat/automations/tools/run-now.ts
|
|
11047
11219
|
/** Create the tool that marks an active scheduled Automation due now. */
|
|
11048
|
-
function createRunAutomationNowTool(context, catalog) {
|
|
11049
|
-
return zodTool({
|
|
11050
|
-
approvalMode: "review",
|
|
11051
|
-
annotations: {
|
|
11052
|
-
destructiveHint: false,
|
|
11053
|
-
idempotentHint: false,
|
|
11054
|
-
openWorldHint: true,
|
|
11055
|
-
readOnlyHint: false
|
|
11056
|
-
},
|
|
11057
|
-
executionMode: "sequential",
|
|
11058
|
-
description: "Queue an existing active scheduled automation to run as soon as possible, without changing its schedule. Use when the user asks to run it now. Anyone who can manage the automation can run it. When it uses its creator's credentials, only the creator can run it now. Event automations run only when a matching event arrives, so they cannot run now.",
|
|
11059
|
-
inputSchema: z.object({ automationId: z.string().min(1).describe("ID of the active scheduled automation to run now, from searchAutomations or its dashboard URL.") }).strict(),
|
|
11060
|
-
outputSchema: automationToolResultSchema.omit({ objectCards: true }),
|
|
11061
|
-
async execute({ automationId }) {
|
|
11062
|
-
const current = await requireToolAutomation(await requireToolCaller(context), automationId);
|
|
11063
|
-
if (current.kind !== "scheduled") throw new ToolInputError("Only scheduled automations can run now. Event automations run when a matching event arrives.");
|
|
11064
|
-
const { task, ownedByViewer } = current;
|
|
11065
|
-
if (task.status !== "active") throw new ToolInputError("Scheduled automation must be active before it can be run now.");
|
|
11066
|
-
requireCredentialStart(task, ownedByViewer);
|
|
11067
|
-
const nowMs = Date.now();
|
|
11068
|
-
const next = {
|
|
11069
|
-
...task,
|
|
11070
|
-
updatedAtMs: nowMs,
|
|
11071
|
-
runNowAtMs: nowMs
|
|
11072
|
-
};
|
|
11073
|
-
await saveScheduledAutomation(getDb(), next, automationRevision(task));
|
|
11074
|
-
return { automation: compactAutomation({
|
|
11075
|
-
...current,
|
|
11076
|
-
task: next
|
|
11077
|
-
}, catalog) };
|
|
11078
|
-
}
|
|
11079
|
-
});
|
|
11080
|
-
}
|
|
11081
|
-
// src/chat/automations/tools/search.ts
|
|
11082
|
-
/**
|
|
11083
|
-
* Create the tool that searches scheduled and event Automations with the
|
|
11084
|
-
* dashboard Automations search. It also finds the Automations that deliver
|
|
11085
|
-
* to this Conversation.
|
|
11086
|
-
*/
|
|
11087
|
-
function createSearchAutomationsTool(context, catalog) {
|
|
11088
|
-
return zodTool({
|
|
11089
|
-
annotations: {
|
|
11090
|
-
destructiveHint: false,
|
|
11091
|
-
idempotentHint: true,
|
|
11092
|
-
openWorldHint: false,
|
|
11093
|
-
readOnlyHint: true
|
|
11094
|
-
},
|
|
11095
|
-
description: "Search scheduled and event automations. It finds the requester's automations, automations in public channels, and automations that deliver to this conversation. Each result has its type and says whether the requester is the creator (isCreator); only the creator can change its credential use. For an event automation, warn when trigger.available is false and explain trigger.problems: the automation stays stored, but a plugin no longer supports its resource type, an event, or a filter. Update the trigger to fix it.",
|
|
11096
|
-
inputSchema: automationSearchInputSchema,
|
|
11097
|
-
outputSchema: automationSearchResultSchema,
|
|
11098
|
-
async execute(input) {
|
|
11099
|
-
const { automations, truncated } = await searchToolAutomations(context, await requireToolCaller(context), input);
|
|
11100
|
-
return {
|
|
11101
|
-
automations: automations.map((candidate) => compactAutomation(candidate, catalog)),
|
|
11102
|
-
truncated
|
|
11103
|
-
};
|
|
11104
|
-
}
|
|
11105
|
-
});
|
|
11106
|
-
}
|
|
11107
|
-
// src/chat/automations/tools/update.ts
|
|
11108
|
-
/** Reject a move while an occurrence is pending or running. */
|
|
11109
|
-
async function requireNoIncompleteRun(automationId) {
|
|
11110
|
-
if ((await getDb().select({ id: juniorAutomationRuns.id }).from(juniorAutomationRuns).where(and(eq(juniorAutomationRuns.automationId, automationId), inArray(juniorAutomationRuns.status, ["pending", "running"]))).limit(1)).length > 0) throw new ToolInputError("Scheduled automation cannot be moved while an occurrence is already running. Try again after the current run finishes.");
|
|
11111
|
-
}
|
|
11112
|
-
/** Edit and save a scheduled Automation, including a move to this Conversation. */
|
|
11113
|
-
async function updateScheduled(context, current, input, editor) {
|
|
11114
|
-
const { task, ownedByViewer: isCreator } = current;
|
|
11115
|
-
const destination = input.destination ? requireToolDestination(context) : void 0;
|
|
11116
|
-
const moving = destination !== void 0 && !sameDestination(task.destination, destination);
|
|
11117
|
-
if (destination && !moving && Object.keys(input).every((key) => key === "destination" || key === "credentialMode" && input.credentialMode === task.credentialMode)) return current;
|
|
11118
|
-
if (moving) await requireNoIncompleteRun(task.id);
|
|
11119
|
-
const next = await editScheduledAutomation(task, {
|
|
11120
|
-
title: input.title,
|
|
11121
|
-
instruction: input.instruction,
|
|
11122
|
-
schedule: input.trigger?.type === "schedule" ? input.trigger.schedule : void 0,
|
|
11123
|
-
credentialMode: input.credentialMode,
|
|
11124
|
-
outcomes: moving ? void 0 : input.outcomes
|
|
11125
|
-
}, isCreator, context.now?.() ?? Date.now());
|
|
11126
|
-
if (moving) {
|
|
11127
|
-
next.destination = destination;
|
|
11128
|
-
next.conversationAccess = getConversationAccess(destination, context.visibility);
|
|
11129
|
-
next.outcomes = input.outcomes === void 0 ? moveTaskOutcomes(task.outcomes, task.destination, destination) : await resolveTaskOutcomes(input.outcomes, destination, task.createdBy.slackUserId);
|
|
11130
|
-
}
|
|
11131
|
-
const saved = await saveScheduledAutomation(getDb(), next, automationRevision(task), editor);
|
|
11132
|
-
if (moving) logInfo("scheduled_automation.move.completed", scheduledAutomationAttributes(saved));
|
|
11133
|
-
return {
|
|
11134
|
-
...current,
|
|
11135
|
-
task: saved
|
|
11136
|
-
};
|
|
11137
|
-
}
|
|
11138
|
-
/** Edit and save an event Automation. */
|
|
11139
|
-
async function updateEvent(current, input, editor, catalog) {
|
|
11140
|
-
const { task } = current;
|
|
11141
|
-
const trigger = input.trigger?.type === "event" ? input.trigger : void 0;
|
|
11142
|
-
const next = await editEventAutomation(task, {
|
|
11143
|
-
title: input.title,
|
|
11144
|
-
instruction: input.instruction,
|
|
11145
|
-
trigger: trigger ? {
|
|
11146
|
-
...trigger,
|
|
11147
|
-
identifier: trigger.identifier === void 0 ? task.trigger.identifier : trigger.identifier ?? void 0,
|
|
11148
|
-
resourceType: trigger.resourceType === void 0 ? task.trigger.resourceType : trigger.resourceType ?? void 0
|
|
11149
|
-
} : void 0,
|
|
11150
|
-
outcomes: input.outcomes,
|
|
11151
|
-
credentialMode: input.credentialMode
|
|
11152
|
-
}, current.ownedByViewer, catalog);
|
|
11153
|
-
const saved = await saveEventAutomation(getDb(), next, automationRevision(task), editor);
|
|
11154
|
-
if (!saved) throw new ToolInputError("Automation was not found.");
|
|
11155
|
-
return {
|
|
11156
|
-
...current,
|
|
11157
|
-
task: saved
|
|
11158
|
-
};
|
|
11159
|
-
}
|
|
11160
|
-
/** Create the tool that edits, pauses, resumes, or moves one Automation. */
|
|
11161
|
-
function createUpdateAutomationTool(context, catalog) {
|
|
11162
|
-
const trigger = automationTriggerSchema(catalog);
|
|
11163
|
-
return zodTool({
|
|
11164
|
-
approvalMode: "review",
|
|
11165
|
-
annotations: {
|
|
11166
|
-
destructiveHint: true,
|
|
11167
|
-
idempotentHint: true,
|
|
11168
|
-
openWorldHint: true,
|
|
11169
|
-
readOnlyHint: false
|
|
11170
|
-
},
|
|
11171
|
-
executionMode: "sequential",
|
|
11172
|
-
description: "Update the title, instruction, trigger, outcomes, credential use, or status of a scheduled or event automation. Anyone in the workspace can update an automation in a public channel. Automations in private channels and DMs can be updated from there or by their creator. Only the creator can enable creator credentials, change outcomes, or move it. A non-creator who changes the instruction or trigger switches it to system credentials. The trigger type cannot change. Set destination to \"current_conversation\" to move a scheduled automation to the channel or DM of this conversation.",
|
|
11173
|
-
inputSchema: z.object({
|
|
11174
|
-
automationId: z.string().min(1).describe("ID of the automation, from searchAutomations or its dashboard URL."),
|
|
11175
|
-
title: automationTitleSchema.optional(),
|
|
11176
|
-
instruction: automationInstructionToolSchema.nullable().optional(),
|
|
11177
|
-
trigger: trigger.nullable().describe("Complete replacement trigger of the same type. For an event trigger, omit identifier or resourceType to keep the saved one. Omit or use null to keep the trigger.").optional(),
|
|
11178
|
-
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(),
|
|
11179
|
-
credentialMode: z.enum(["system", "creator"]).nullable().describe("Set creator to make the automation creator's credentials available, or system to disable them. Creator always means the automation's createdBy actor, never the current requester. Only that creator may enable creator mode. Omit or use null to leave unchanged.").optional(),
|
|
11180
|
-
status: z.enum(["active", "paused"]).nullable().describe("Set active to resume a paused or blocked automation, or paused to stop it. When it uses its creator's credentials, only the creator can resume it. Omit or use null to leave unchanged.").optional(),
|
|
11181
|
-
destination: z.literal("current_conversation").nullable().describe("Set to \"current_conversation\" to move the creator's scheduled automation into the channel or DM of this conversation. Omit to keep its destination.").optional()
|
|
11182
|
-
}).strict(),
|
|
11183
|
-
prepareArguments(args) {
|
|
11184
|
-
return Object.fromEntries(Object.entries(args).filter(([, value]) => value != null));
|
|
11185
|
-
},
|
|
11186
|
-
outputSchema: automationToolResultSchema,
|
|
11187
|
-
async execute({ automationId, status: requestedStatus, ...fields }) {
|
|
11188
|
-
const input = fields;
|
|
11189
|
-
const status = requestedStatus ?? void 0;
|
|
11190
|
-
const caller = await requireToolCaller(context);
|
|
11191
|
-
const current = await requireToolAutomation(caller, automationId);
|
|
11192
|
-
if (input.trigger && triggerKind(input.trigger) !== current.kind) throw new ToolInputError(`This automation runs on ${current.kind === "scheduled" ? "a schedule" : "events"}. The trigger type cannot change. Create a new automation instead.`);
|
|
11193
|
-
if (Object.keys(input).length === 0 && status === void 0) throw new ToolInputError("Automation update requires a change.");
|
|
11194
|
-
if (current.kind === "scheduled" && current.task.status === "completed") throw new ToolInputError("Completed scheduled automations cannot be updated. Create a new automation instead.");
|
|
11195
|
-
if (input.destination && !current.ownedByViewer) throw new ToolInputError("Only the automation creator can move this automation.");
|
|
11196
|
-
if (input.destination && current.kind === "event") throw new ToolInputError("Event automations cannot move. Create a new event automation in this conversation instead.");
|
|
11197
|
-
const editor = automationEditor(caller.viewer, current);
|
|
11198
|
-
if (input.instruction !== void 0 && input.instruction !== current.task.task.text) requireInstructionReferences(input.instruction, {
|
|
11199
|
-
creator: current.task.createdBy,
|
|
11200
|
-
destinationChannelId: current.task.destination.channelId,
|
|
11201
|
-
requester: editor,
|
|
11202
|
-
previousInstruction: current.task.task.text
|
|
11203
|
-
});
|
|
11204
|
-
let saved = current;
|
|
11205
|
-
if (Object.keys(input).length > 0) saved = current.kind === "scheduled" ? await updateScheduled(context, current, input, editor) : await updateEvent(current, input, editor, catalog);
|
|
11206
|
-
if (status !== void 0 && status !== saved.task.status) saved = await changeAutomationLifecycle(saved, {
|
|
11207
|
-
action: status === "paused" ? "pause" : "resume",
|
|
11208
|
-
revision: automationRevision(saved.task)
|
|
11209
|
-
});
|
|
11210
|
-
return automationToolResult(context.conversationId, saved, catalog);
|
|
11220
|
+
function createRunAutomationNowTool(context, catalog) {
|
|
11221
|
+
return zodTool({
|
|
11222
|
+
approvalMode: "review",
|
|
11223
|
+
annotations: {
|
|
11224
|
+
destructiveHint: false,
|
|
11225
|
+
idempotentHint: false,
|
|
11226
|
+
openWorldHint: true,
|
|
11227
|
+
readOnlyHint: false
|
|
11228
|
+
},
|
|
11229
|
+
executionMode: "sequential",
|
|
11230
|
+
description: "Queue an existing active scheduled automation to run as soon as possible, without changing its schedule. Use when the user asks to run it now. Anyone who can manage the automation can run it. When it uses its creator's credentials, only the creator can run it now. Event automations run only when a matching event arrives, so they cannot run now.",
|
|
11231
|
+
inputSchema: z.object({ automationId: z.string().min(1).describe("ID of the active scheduled automation to run now, from searchAutomations or its dashboard URL.") }).strict(),
|
|
11232
|
+
outputSchema: automationToolResultSchema.omit({ objectCards: true }),
|
|
11233
|
+
async execute({ automationId }) {
|
|
11234
|
+
const current = await requireToolAutomation(await requireToolCaller(context), automationId);
|
|
11235
|
+
if (current.kind !== "scheduled") throw new ToolInputError("Only scheduled automations can run now. Event automations run when a matching event arrives.");
|
|
11236
|
+
const { task, ownedByViewer } = current;
|
|
11237
|
+
if (task.status !== "active") throw new ToolInputError("Scheduled automation must be active before it can be run now.");
|
|
11238
|
+
requireCredentialStart(task, ownedByViewer);
|
|
11239
|
+
const nowMs = Date.now();
|
|
11240
|
+
const next = {
|
|
11241
|
+
...task,
|
|
11242
|
+
updatedAtMs: nowMs,
|
|
11243
|
+
runNowAtMs: nowMs
|
|
11244
|
+
};
|
|
11245
|
+
await saveScheduledAutomation(getDb(), next, automationRevision(task));
|
|
11246
|
+
return { automation: compactAutomation({
|
|
11247
|
+
...current,
|
|
11248
|
+
task: next
|
|
11249
|
+
}, catalog) };
|
|
11211
11250
|
}
|
|
11212
11251
|
});
|
|
11213
11252
|
}
|
|
11214
|
-
// src/chat/tools/
|
|
11215
|
-
/** Distinguish legacy TypeBox schemas from JSON Schema projected from Zod. */
|
|
11216
|
-
function isTypeBoxInputSchema(schema) {
|
|
11217
|
-
return typeof schema === "object" && schema !== null && Kind in schema;
|
|
11218
|
-
}
|
|
11219
|
-
/** Tool error text for a call to a tool that is disabled in this Turn. */
|
|
11220
|
-
function disabledToolMessage(toolName, reason) {
|
|
11221
|
-
return `${toolName} is disabled in this Turn. ${reason} Do not call it again in this Turn.`;
|
|
11222
|
-
}
|
|
11253
|
+
// src/chat/automations/tools/search.ts
|
|
11223
11254
|
/**
|
|
11224
|
-
*
|
|
11225
|
-
*
|
|
11226
|
-
*
|
|
11227
|
-
* unset reason keeps the tools enabled.
|
|
11255
|
+
* Create the tool that searches scheduled and event Automations with the
|
|
11256
|
+
* dashboard Automations search. It also finds the Automations that deliver
|
|
11257
|
+
* to this Conversation.
|
|
11228
11258
|
*/
|
|
11229
|
-
function
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11259
|
+
function createSearchAutomationsTool(context, catalog) {
|
|
11260
|
+
return zodTool({
|
|
11261
|
+
annotations: {
|
|
11262
|
+
destructiveHint: false,
|
|
11263
|
+
idempotentHint: true,
|
|
11264
|
+
openWorldHint: false,
|
|
11265
|
+
readOnlyHint: true
|
|
11266
|
+
},
|
|
11267
|
+
description: "Search scheduled and event automations. It finds the requester's automations, automations in public channels, and automations that deliver to this conversation. Each result has its type and says whether the requester is the creator (isCreator); only the creator can change its credential use. For an event automation, warn when trigger.available is false and explain trigger.problems: the automation stays stored, but a plugin no longer supports its resource type, an event, or a filter. Update the trigger to fix it.",
|
|
11268
|
+
inputSchema: automationSearchInputSchema,
|
|
11269
|
+
outputSchema: automationSearchResultSchema,
|
|
11270
|
+
async execute(input) {
|
|
11271
|
+
const { automations, truncated } = await searchToolAutomations(context, await requireToolCaller(context), input);
|
|
11272
|
+
return {
|
|
11273
|
+
automations: automations.map((candidate) => compactAutomation(candidate, catalog)),
|
|
11274
|
+
truncated
|
|
11275
|
+
};
|
|
11276
|
+
}
|
|
11277
|
+
});
|
|
11235
11278
|
}
|
|
11236
|
-
// src/chat/
|
|
11237
|
-
/**
|
|
11238
|
-
function
|
|
11239
|
-
|
|
11240
|
-
return
|
|
11241
|
-
|
|
11242
|
-
|
|
11243
|
-
|
|
11244
|
-
|
|
11245
|
-
|
|
11246
|
-
}, automationToolsDisabledReason(context));
|
|
11279
|
+
// src/chat/conversations/destination-visibility.ts
|
|
11280
|
+
/** Read confirmed visibility from the current signal or persisted destination. */
|
|
11281
|
+
async function resolveDestinationVisibility(args) {
|
|
11282
|
+
if (args.visibility) return args.visibility;
|
|
11283
|
+
if (args.destination.platform === "local") return "private";
|
|
11284
|
+
return await getConversationStore().getDestinationVisibility({
|
|
11285
|
+
provider: "slack",
|
|
11286
|
+
providerDestinationId: args.destination.channelId,
|
|
11287
|
+
providerTenantId: args.destination.teamId
|
|
11288
|
+
});
|
|
11247
11289
|
}
|
|
11248
11290
|
// src/chat/slack/channel.ts
|
|
11249
11291
|
const replySubtypeSchema = z.object({ subtype: z.string().optional() });
|
|
@@ -11457,6 +11499,203 @@ async function resolveOptionalSlackChannelRef(input) {
|
|
|
11457
11499
|
if (input.defaultChannelId) return { channelId: input.defaultChannelId };
|
|
11458
11500
|
throw new ToolInputError("Provide `channel_id`, or use this tool in an active Slack channel context.");
|
|
11459
11501
|
}
|
|
11502
|
+
// src/chat/automations/tools/update.ts
|
|
11503
|
+
/** Reject a move while an occurrence is pending or running. */
|
|
11504
|
+
async function requireNoIncompleteRun(automationId) {
|
|
11505
|
+
if ((await getDb().select({ id: juniorAutomationRuns.id }).from(juniorAutomationRuns).where(and(eq(juniorAutomationRuns.automationId, automationId), inArray(juniorAutomationRuns.status, ["pending", "running"]))).limit(1)).length > 0) throw new ToolInputError("Scheduled automation cannot be moved while an occurrence is already running. Try again after the current run finishes.");
|
|
11506
|
+
}
|
|
11507
|
+
/**
|
|
11508
|
+
* Resolve where a move goes. "current_conversation" is this Conversation. A
|
|
11509
|
+
* channel reference must name a public Slack channel in this workspace, and
|
|
11510
|
+
* Junior joins it so the Automation can post there.
|
|
11511
|
+
*/
|
|
11512
|
+
async function requireMoveDestination(context, value) {
|
|
11513
|
+
const here = requireToolDestination(context);
|
|
11514
|
+
if (value === "current_conversation") return {
|
|
11515
|
+
destination: here,
|
|
11516
|
+
visibility: context.visibility
|
|
11517
|
+
};
|
|
11518
|
+
const teamId = parseSlackTeamId(here.teamId);
|
|
11519
|
+
if (!teamId) throw new ToolInputError("Slack workspace is unavailable.");
|
|
11520
|
+
const { channelId, channelName } = await resolveSlackChannelRef({
|
|
11521
|
+
field: "destination",
|
|
11522
|
+
value,
|
|
11523
|
+
teamId
|
|
11524
|
+
});
|
|
11525
|
+
if (channelId === here.channelId) return {
|
|
11526
|
+
destination: here,
|
|
11527
|
+
visibility: context.visibility
|
|
11528
|
+
};
|
|
11529
|
+
const access = await checkSlackChannelReadAccess({
|
|
11530
|
+
currentChannelIds: [],
|
|
11531
|
+
targetChannelId: channelId,
|
|
11532
|
+
teamId
|
|
11533
|
+
});
|
|
11534
|
+
if (!access.allowed) throw new ToolInputError(access.reason === "private" ? "Only a public channel can be named as a destination. To move an automation into a private channel or DM, ask from there." : access.error);
|
|
11535
|
+
if (access.isMember !== true) {
|
|
11536
|
+
const joined = await joinPublicChannelForRead({
|
|
11537
|
+
targetChannelId: channelId,
|
|
11538
|
+
...channelName ? { channelName } : void 0
|
|
11539
|
+
});
|
|
11540
|
+
if (!joined.ok) throw new ToolInputError(joined.error);
|
|
11541
|
+
}
|
|
11542
|
+
const destination = {
|
|
11543
|
+
platform: "slack",
|
|
11544
|
+
teamId,
|
|
11545
|
+
channelId
|
|
11546
|
+
};
|
|
11547
|
+
const name = access.channelName ?? channelName;
|
|
11548
|
+
await getConversationStore().recordDestinationVisibility({
|
|
11549
|
+
destination,
|
|
11550
|
+
...name ? { channelName: name } : void 0,
|
|
11551
|
+
visibility: "public"
|
|
11552
|
+
});
|
|
11553
|
+
return {
|
|
11554
|
+
destination,
|
|
11555
|
+
visibility: "public"
|
|
11556
|
+
};
|
|
11557
|
+
}
|
|
11558
|
+
/** Edit and save a scheduled Automation, including a move to a new Destination. */
|
|
11559
|
+
async function updateScheduled(context, current, input, editor) {
|
|
11560
|
+
const { task, ownedByViewer: isCreator } = current;
|
|
11561
|
+
const target = input.destination ? await requireMoveDestination(context, input.destination) : void 0;
|
|
11562
|
+
const destination = target?.destination;
|
|
11563
|
+
const moving = destination !== void 0 && !sameDestination(task.destination, destination);
|
|
11564
|
+
if (destination && !moving && Object.keys(input).every((key) => key === "destination" || key === "credentialMode" && input.credentialMode === task.credentialMode)) return current;
|
|
11565
|
+
if (moving) await requireNoIncompleteRun(task.id);
|
|
11566
|
+
const next = await editScheduledAutomation(task, {
|
|
11567
|
+
title: input.title,
|
|
11568
|
+
instruction: input.instruction,
|
|
11569
|
+
schedule: input.trigger?.type === "schedule" ? input.trigger.schedule : void 0,
|
|
11570
|
+
credentialMode: input.credentialMode,
|
|
11571
|
+
outcomes: moving ? void 0 : input.outcomes
|
|
11572
|
+
}, isCreator, context.now?.() ?? Date.now());
|
|
11573
|
+
if (moving) {
|
|
11574
|
+
next.destination = destination;
|
|
11575
|
+
next.conversationAccess = getConversationAccess(destination, target?.visibility);
|
|
11576
|
+
if (!isCreator) next.credentialMode = "system";
|
|
11577
|
+
next.outcomes = input.outcomes === void 0 ? moveTaskOutcomes(task.outcomes, task.destination, destination) : await resolveTaskOutcomes(input.outcomes, destination, task.createdBy.slackUserId);
|
|
11578
|
+
}
|
|
11579
|
+
const saved = await saveScheduledAutomation(getDb(), next, automationRevision(task), editor);
|
|
11580
|
+
if (moving) logInfo("scheduled_automation.move.completed", scheduledAutomationAttributes(saved));
|
|
11581
|
+
return {
|
|
11582
|
+
...current,
|
|
11583
|
+
task: saved
|
|
11584
|
+
};
|
|
11585
|
+
}
|
|
11586
|
+
/** Edit and save an event Automation. */
|
|
11587
|
+
async function updateEvent(current, input, editor, catalog) {
|
|
11588
|
+
const { task } = current;
|
|
11589
|
+
const trigger = input.trigger?.type === "event" ? input.trigger : void 0;
|
|
11590
|
+
const next = await editEventAutomation(task, {
|
|
11591
|
+
title: input.title,
|
|
11592
|
+
instruction: input.instruction,
|
|
11593
|
+
trigger: trigger ? {
|
|
11594
|
+
...trigger,
|
|
11595
|
+
...trigger.namespace === task.trigger.namespace ? {
|
|
11596
|
+
identifier: task.trigger.identifier,
|
|
11597
|
+
resourceType: task.trigger.resourceType
|
|
11598
|
+
} : void 0,
|
|
11599
|
+
match: changedEventMatch(task.trigger, trigger.namespace, trigger.match)
|
|
11600
|
+
} : void 0,
|
|
11601
|
+
outcomes: input.outcomes,
|
|
11602
|
+
credentialMode: input.credentialMode
|
|
11603
|
+
}, current.ownedByViewer, catalog);
|
|
11604
|
+
const saved = await saveEventAutomation(getDb(), next, automationRevision(task), editor);
|
|
11605
|
+
if (!saved) throw new ToolInputError("Automation was not found.");
|
|
11606
|
+
return {
|
|
11607
|
+
...current,
|
|
11608
|
+
task: saved
|
|
11609
|
+
};
|
|
11610
|
+
}
|
|
11611
|
+
/** Create the tool that edits, pauses, resumes, or moves one Automation. */
|
|
11612
|
+
function createUpdateAutomationTool(context, catalog) {
|
|
11613
|
+
const trigger = automationTriggerChangesSchema(catalog);
|
|
11614
|
+
return zodTool({
|
|
11615
|
+
approvalMode: "review",
|
|
11616
|
+
annotations: {
|
|
11617
|
+
destructiveHint: true,
|
|
11618
|
+
idempotentHint: true,
|
|
11619
|
+
openWorldHint: true,
|
|
11620
|
+
readOnlyHint: false
|
|
11621
|
+
},
|
|
11622
|
+
executionMode: "sequential",
|
|
11623
|
+
description: "Update the title, instruction, trigger, outcomes, credential use, or status of a scheduled or event automation. Anyone in the workspace can update anything on an automation in a public channel, including its outcomes and destination. Automations in private channels and DMs can be updated from there or by their creator, and only their creator can move them. Only the creator can enable creator credentials. A non-creator who changes the instruction, trigger, outcomes, or destination switches it to system credentials. The trigger type cannot change. Set destination to move a scheduled automation to this conversation or to a public channel.",
|
|
11624
|
+
inputSchema: z.object({
|
|
11625
|
+
automationId: z.string().min(1).describe("ID of the automation, from searchAutomations or its dashboard URL."),
|
|
11626
|
+
title: automationTitleSchema.optional(),
|
|
11627
|
+
instruction: automationInstructionToolSchema.nullable().optional(),
|
|
11628
|
+
trigger: trigger.nullable().describe("Replacement trigger of the same type. For an event trigger, match changes only the filters that it names. Omit or use null to keep the trigger.").optional(),
|
|
11629
|
+
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(),
|
|
11630
|
+
credentialMode: z.enum(["system", "creator"]).nullable().describe("Set creator to make the automation creator's credentials available, or system to disable them. Creator always means the automation's createdBy actor, never the current requester. Only that creator may enable creator mode. Omit or use null to leave unchanged.").optional(),
|
|
11631
|
+
status: z.enum(["active", "paused"]).nullable().describe("Set active to resume a paused or blocked automation, or paused to stop it. When it uses its creator's credentials, only the creator can resume it. Omit or use null to leave unchanged.").optional(),
|
|
11632
|
+
destination: slackChannelRefParam.nullable().describe("Where to move a scheduled automation. Set \"current_conversation\" for the channel or DM of this conversation, or name a public Slack channel by id, mention, or known name. Omit to keep its destination.").optional()
|
|
11633
|
+
}).strict(),
|
|
11634
|
+
prepareArguments(args) {
|
|
11635
|
+
return Object.fromEntries(Object.entries(args).filter(([, value]) => value != null));
|
|
11636
|
+
},
|
|
11637
|
+
outputSchema: automationToolResultSchema,
|
|
11638
|
+
async execute({ automationId, status: requestedStatus, ...fields }) {
|
|
11639
|
+
const input = fields;
|
|
11640
|
+
const status = requestedStatus ?? void 0;
|
|
11641
|
+
const caller = await requireToolCaller(context);
|
|
11642
|
+
const current = await requireToolAutomation(caller, automationId);
|
|
11643
|
+
if (input.trigger && triggerKind(input.trigger) !== current.kind) throw new ToolInputError(`This automation runs on ${current.kind === "scheduled" ? "a schedule" : "events"}. The trigger type cannot change. Create a new automation instead.`);
|
|
11644
|
+
if (Object.keys(input).length === 0 && status === void 0) throw new ToolInputError("Automation update requires a change.");
|
|
11645
|
+
if (current.kind === "scheduled" && current.task.status === "completed") throw new ToolInputError("Completed scheduled automations cannot be updated. Create a new automation instead.");
|
|
11646
|
+
if (input.destination && current.kind === "event") throw new ToolInputError("Event automations cannot move. Create a new event automation in this conversation instead.");
|
|
11647
|
+
if (input.destination && !current.ownedByViewer && await resolveDestinationVisibility({ destination: current.task.destination }) !== "public") throw new ToolInputError("Only the creator can move an automation out of a private channel or DM.");
|
|
11648
|
+
const editor = automationEditor(caller.viewer, current);
|
|
11649
|
+
if (input.instruction !== void 0 && input.instruction !== current.task.task.text) requireInstructionReferences(input.instruction, {
|
|
11650
|
+
creator: current.task.createdBy,
|
|
11651
|
+
destinationChannelId: current.task.destination.channelId,
|
|
11652
|
+
requester: editor,
|
|
11653
|
+
previousInstruction: current.task.task.text
|
|
11654
|
+
});
|
|
11655
|
+
let saved = current;
|
|
11656
|
+
if (Object.keys(input).length > 0) saved = current.kind === "scheduled" ? await updateScheduled(context, current, input, editor) : await updateEvent(current, input, editor, catalog);
|
|
11657
|
+
if (status !== void 0 && status !== saved.task.status) saved = await changeAutomationLifecycle(saved, {
|
|
11658
|
+
action: status === "paused" ? "pause" : "resume",
|
|
11659
|
+
revision: automationRevision(saved.task)
|
|
11660
|
+
});
|
|
11661
|
+
return automationToolResult(context.conversationId, saved, catalog);
|
|
11662
|
+
}
|
|
11663
|
+
});
|
|
11664
|
+
}
|
|
11665
|
+
// src/chat/tools/definition.ts
|
|
11666
|
+
/** Distinguish legacy TypeBox schemas from JSON Schema projected from Zod. */
|
|
11667
|
+
function isTypeBoxInputSchema(schema) {
|
|
11668
|
+
return typeof schema === "object" && schema !== null && Kind in schema;
|
|
11669
|
+
}
|
|
11670
|
+
/** Tool error text for a call to a tool that is disabled in this Turn. */
|
|
11671
|
+
function disabledToolMessage(toolName, reason) {
|
|
11672
|
+
return `${toolName} is disabled in this Turn. ${reason} Do not call it again in this Turn.`;
|
|
11673
|
+
}
|
|
11674
|
+
/**
|
|
11675
|
+
* Disable tools for this Turn without removing them from the tool list.
|
|
11676
|
+
*
|
|
11677
|
+
* The tool adapter rejects their calls, and the Turn context lists them. An
|
|
11678
|
+
* unset reason keeps the tools enabled.
|
|
11679
|
+
*/
|
|
11680
|
+
function disableTools(tools, reason) {
|
|
11681
|
+
if (!reason) return tools;
|
|
11682
|
+
return Object.fromEntries(Object.entries(tools).map(([name, tool]) => [name, {
|
|
11683
|
+
...tool,
|
|
11684
|
+
disabledReason: reason
|
|
11685
|
+
}]));
|
|
11686
|
+
}
|
|
11687
|
+
// src/chat/tools/automations.ts
|
|
11688
|
+
/** Build the Automation tools. A Turn that no person started cannot use them. */
|
|
11689
|
+
function createAutomationTools(context, catalog) {
|
|
11690
|
+
const automationContext = automationToolContext(context);
|
|
11691
|
+
return disableTools({
|
|
11692
|
+
createAutomation: createAutomationTool(automationContext, catalog),
|
|
11693
|
+
searchAutomations: createSearchAutomationsTool(automationContext, catalog),
|
|
11694
|
+
updateAutomation: createUpdateAutomationTool(automationContext, catalog),
|
|
11695
|
+
deleteAutomation: createDeleteAutomationTool(automationContext, catalog),
|
|
11696
|
+
runAutomationNow: createRunAutomationNowTool(automationContext, catalog)
|
|
11697
|
+
}, automationToolsDisabledReason(context));
|
|
11698
|
+
}
|
|
11460
11699
|
// src/chat/slack/tools/channel-join.ts
|
|
11461
11700
|
/** Create a tool that joins one public Slack channel. */
|
|
11462
11701
|
function createSlackChannelJoinTool(context) {
|
|
@@ -12388,7 +12627,6 @@ const SLACK_REVIEW_ITEM_VIEW_ID = "junior_review_item";
|
|
|
12388
12627
|
const ACTION_ID_PREFIX = "junior_review_item:";
|
|
12389
12628
|
const CHOICE_ACTION_ID_PREFIX = `${ACTION_ID_PREFIX}choice:`;
|
|
12390
12629
|
const ANSWER_ACTION_ID = `${ACTION_ID_PREFIX}answer`;
|
|
12391
|
-
const DONE_ACTION_ID = `${ACTION_ID_PREFIX}done`;
|
|
12392
12630
|
const DISMISS_ACTION_ID = `${ACTION_ID_PREFIX}dismiss`;
|
|
12393
12631
|
const MAX_BODY_CHARS = 3e3;
|
|
12394
12632
|
const MAX_CONTEXT_CHARS = 3e3;
|
|
@@ -12424,13 +12662,7 @@ function controls(item) {
|
|
|
12424
12662
|
text: plainText("Dismiss"),
|
|
12425
12663
|
value: item.id
|
|
12426
12664
|
};
|
|
12427
|
-
if (item.kind === "notice" || !item.request) return [
|
|
12428
|
-
type: "button",
|
|
12429
|
-
action_id: DONE_ACTION_ID,
|
|
12430
|
-
style: "primary",
|
|
12431
|
-
text: plainText("Done"),
|
|
12432
|
-
value: item.id
|
|
12433
|
-
}, dismiss];
|
|
12665
|
+
if (item.kind === "notice" || !item.request) return [dismiss];
|
|
12434
12666
|
const buttonField = buttonUserInputField(item.request);
|
|
12435
12667
|
if (!buttonField) return [{
|
|
12436
12668
|
type: "button",
|
|
@@ -12475,8 +12707,8 @@ function closedNote(item) {
|
|
|
12475
12707
|
/**
|
|
12476
12708
|
* Render a Review item as one Slack message. An open item shows its
|
|
12477
12709
|
* controls: one button per choice when one click can answer it, an Answer
|
|
12478
|
-
* button that opens a form otherwise, and
|
|
12479
|
-
* shows a note instead, so nobody can act on it again.
|
|
12710
|
+
* button that opens a form otherwise, and only Dismiss for a notice. A
|
|
12711
|
+
* closed item shows a note instead, so nobody can act on it again.
|
|
12480
12712
|
*/
|
|
12481
12713
|
function buildSlackReviewItemCard(item) {
|
|
12482
12714
|
const text = itemText(item);
|
|
@@ -12544,10 +12776,6 @@ function parseSlackReviewItemClick(action) {
|
|
|
12544
12776
|
kind: "answer",
|
|
12545
12777
|
itemId: value
|
|
12546
12778
|
};
|
|
12547
|
-
if (action.action_id === DONE_ACTION_ID) return {
|
|
12548
|
-
kind: "done",
|
|
12549
|
-
itemId: value
|
|
12550
|
-
};
|
|
12551
12779
|
if (action.action_id === DISMISS_ACTION_ID) return {
|
|
12552
12780
|
kind: "dismiss",
|
|
12553
12781
|
itemId: value
|
|
@@ -16668,6 +16896,12 @@ function createToolActionReview(options) {
|
|
|
16668
16896
|
const pendingRejections = /* @__PURE__ */ new Map();
|
|
16669
16897
|
let consecutiveRejections = 0;
|
|
16670
16898
|
/**
|
|
16899
|
+
* Rejection state of each attempt, so an attempt counts only once. Pi runs
|
|
16900
|
+
* a `codemode` call alone, so no other attempt changes the count while
|
|
16901
|
+
* its batches run.
|
|
16902
|
+
*/
|
|
16903
|
+
const attempts = /* @__PURE__ */ new Map();
|
|
16904
|
+
/**
|
|
16671
16905
|
* Review one action and record its decision. Gate state does not change
|
|
16672
16906
|
* here, so concurrent reviews in one batch cannot corrupt it.
|
|
16673
16907
|
*/
|
|
@@ -16694,8 +16928,50 @@ function createToolActionReview(options) {
|
|
|
16694
16928
|
};
|
|
16695
16929
|
}
|
|
16696
16930
|
};
|
|
16931
|
+
/**
|
|
16932
|
+
* Count each attempt in the batch once, in batch order. An attempt with a
|
|
16933
|
+
* rejection adds one to the count from before the attempt, also when it
|
|
16934
|
+
* has allowed actions or spans several batches. An attempt with only
|
|
16935
|
+
* allowed actions resets the count. Skipped and unavailable actions do not
|
|
16936
|
+
* change it. Return the attempt state of each action.
|
|
16937
|
+
*/
|
|
16938
|
+
const countAttempts = (actions, steps) => {
|
|
16939
|
+
const states = actions.map((action) => {
|
|
16940
|
+
const id = action.attemptId ?? action.toolCallId;
|
|
16941
|
+
let state = attempts.get(id);
|
|
16942
|
+
if (!state) {
|
|
16943
|
+
state = {
|
|
16944
|
+
before: consecutiveRejections,
|
|
16945
|
+
exhausted: false,
|
|
16946
|
+
rejected: false
|
|
16947
|
+
};
|
|
16948
|
+
attempts.set(id, state);
|
|
16949
|
+
}
|
|
16950
|
+
return state;
|
|
16951
|
+
});
|
|
16952
|
+
states.forEach((state, index) => {
|
|
16953
|
+
const step = steps[index];
|
|
16954
|
+
if (step.kind !== "reviewed" || state.rejected) return;
|
|
16955
|
+
if (step.decision.decision === "allow") {
|
|
16956
|
+
consecutiveRejections = 0;
|
|
16957
|
+
return;
|
|
16958
|
+
}
|
|
16959
|
+
state.rejected = true;
|
|
16960
|
+
consecutiveRejections = state.before + 1;
|
|
16961
|
+
if (consecutiveRejections < MAX_CONSECUTIVE_REJECTIONS) return;
|
|
16962
|
+
state.exhausted = true;
|
|
16963
|
+
logException(new ToolActionReviewLimitError(), "guardian.action_review.exhausted", {
|
|
16964
|
+
"app.guardian.decision": step.decision.decision,
|
|
16965
|
+
"app.guardian.risk_level": step.decision.riskLevel,
|
|
16966
|
+
"app.guardian.user_authorization": step.decision.userAuthorization,
|
|
16967
|
+
"gen_ai.tool.name": step.proposal.tool.name,
|
|
16968
|
+
"gen_ai.tool.call.id": actions[index].toolCallId
|
|
16969
|
+
});
|
|
16970
|
+
});
|
|
16971
|
+
return states;
|
|
16972
|
+
};
|
|
16697
16973
|
/** Apply one review step to gate state and return the action's result. */
|
|
16698
|
-
const settle = (toolCallId, step) => {
|
|
16974
|
+
const settle = (toolCallId, step, exhausted) => {
|
|
16699
16975
|
if (step.kind === "skipped") return { allowed: true };
|
|
16700
16976
|
if (step.kind === "unavailable") {
|
|
16701
16977
|
options.onFatal(step.error);
|
|
@@ -16705,24 +16981,12 @@ function createToolActionReview(options) {
|
|
|
16705
16981
|
};
|
|
16706
16982
|
}
|
|
16707
16983
|
const { decision, proposal } = step;
|
|
16708
|
-
if (decision.decision === "allow") {
|
|
16709
|
-
|
|
16710
|
-
|
|
16711
|
-
|
|
16712
|
-
decision
|
|
16713
|
-
};
|
|
16714
|
-
}
|
|
16984
|
+
if (decision.decision === "allow") return {
|
|
16985
|
+
allowed: true,
|
|
16986
|
+
decision
|
|
16987
|
+
};
|
|
16715
16988
|
const reviewedAction = projectedRejection(proposal, decision);
|
|
16716
16989
|
addPriorRejection(priorRejections, reviewedAction);
|
|
16717
|
-
consecutiveRejections += 1;
|
|
16718
|
-
const exhausted = consecutiveRejections >= MAX_CONSECUTIVE_REJECTIONS;
|
|
16719
|
-
if (exhausted) logException(new ToolActionReviewLimitError(), "guardian.action_review.exhausted", {
|
|
16720
|
-
"app.guardian.decision": decision.decision,
|
|
16721
|
-
"app.guardian.risk_level": decision.riskLevel,
|
|
16722
|
-
"app.guardian.user_authorization": decision.userAuthorization,
|
|
16723
|
-
"gen_ai.tool.name": proposal.tool.name,
|
|
16724
|
-
"gen_ai.tool.call.id": toolCallId
|
|
16725
|
-
});
|
|
16726
16990
|
pendingRejections.set(toolCallId, {
|
|
16727
16991
|
decision: decision.decision,
|
|
16728
16992
|
priorRejection: reviewedAction,
|
|
@@ -16749,7 +17013,8 @@ function createToolActionReview(options) {
|
|
|
16749
17013
|
return await withSpan("guardian.action_review", "guardian.action_review", {}, async () => {
|
|
16750
17014
|
const rejections = [...priorRejections];
|
|
16751
17015
|
const steps = await mapConcurrent(actions, MAX_CONCURRENT_REVIEWS, (action) => reviewStep(action, rejections, signal));
|
|
16752
|
-
|
|
17016
|
+
const states = countAttempts(actions, steps);
|
|
17017
|
+
return new Map(actions.map((action, index) => [action.toolCallId, settle(action.toolCallId, steps[index], states[index].exhausted)]));
|
|
16753
17018
|
}, { "app.guardian.batch.size": actions.length });
|
|
16754
17019
|
},
|
|
16755
17020
|
projectToolResult(toolCallId, result) {
|
|
@@ -17457,6 +17722,7 @@ async function runCodeModeScript(run) {
|
|
|
17457
17722
|
});
|
|
17458
17723
|
return await run.pipeline.reviewScriptCalls(actions.map((action) => ({
|
|
17459
17724
|
...action,
|
|
17725
|
+
attemptId: run.toolCallId,
|
|
17460
17726
|
script: {
|
|
17461
17727
|
source: run.source,
|
|
17462
17728
|
calls: earlier
|
|
@@ -17619,7 +17885,8 @@ function renderScriptTools(tools) {
|
|
|
17619
17885
|
"declare const tools: {",
|
|
17620
17886
|
...shown.map((entry) => entry.text),
|
|
17621
17887
|
"};",
|
|
17622
|
-
...omitted > 0 ? [`${omitted} more tools are not declared here.
|
|
17888
|
+
...omitted > 0 ? [`${omitted} more tools are not declared here. To find one, call ${SEARCH_TOOLS_NAME} directly before the script. Then call each result as \`tools.<tool_name>(args)\`, with its input_schema. A script cannot call ${SEARCH_TOOLS_NAME} or ${EXECUTE_TOOL_NAME}.`] : [],
|
|
17889
|
+
...Object.hasOwn(tools, "callMcpTool") ? ["Call an MCP tool as `tools.callMcpTool({ tool_name, arguments })`. `tools.mcp__<provider>__<tool>` does not exist."] : []
|
|
17623
17890
|
].join("\n");
|
|
17624
17891
|
}
|
|
17625
17892
|
function renderDescription(tools) {
|
|
@@ -21211,6 +21478,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
|
|
|
21211
21478
|
};
|
|
21212
21479
|
agent = new Agent({
|
|
21213
21480
|
getApiKey: getGatewayApiKey,
|
|
21481
|
+
sessionId: conversationId,
|
|
21214
21482
|
streamFn: createTracedStreamFn({
|
|
21215
21483
|
conversationPrivacy,
|
|
21216
21484
|
...streamFn ? { base: streamFn } : void 0
|
|
@@ -21664,4 +21932,4 @@ function createAgentRunner(execute, options) {
|
|
|
21664
21932
|
return await execute(nextRun, streamFn);
|
|
21665
21933
|
} };
|
|
21666
21934
|
}
|
|
21667
|
-
export {
|
|
21935
|
+
export { AutomationEditError as $, verifySlackSignature as A, saveUserCustomization as At, scheduledAutomationRunAttributes as B, syncReviewItemSlackCards as C, conversationSubtree as Ct, parseSlackReviewItemFormTarget as D, buildSteeringPiMessage as Dt, parseSlackReviewItemClick as E, ingestEvent as Et, getConversationInfo as F, shouldEmitDevAgentTrace as Ft, updateViewerAutomation as G, changeViewerAutomationLifecycle as H, joinPublicChannel as I, readViewerAutomationExecutions as J, ViewerTaskNotFoundError as K, listThreadReplies as L, readSlackRawMessageContext as M, actorFromRun as Mt, resolveSlackChannelIdFromMessage as N, toolInvocationDestination as Nt, parseSlackUserInputSubmission as O, withoutSavedSteeringMessages as Ot, resolveSlackChannelIdFromThreadId as P, readBlockedReport as Pt, readViewerAutomations as Q, resolveDestinationVisibility as R, readSlackReviewItemCardConversationId as S, listUnreportedSiblingAgents as St, buildSlackReviewItemModal as T, processWorkspaceSnapshotJob as Tt, deleteViewerTask as U, activateViewerAutomationVersion as V, readViewerAutomationEdit as W, readViewerAutomationSummary as X, readViewerAutomationRuns as Y, readViewerAutomationVersions as Z, unpublishedArtifactHeaders as _, stopChildAgentsOf as _t, finalizeMcpAuthorization as a, appendAndEnqueueWebMessage as at, renderBlockText as b, recordSubagentStarted as bt, deleteMcpServerSessionId as c, createConversationId as ct, getMcpStoredOAuthCredentials as d, resolveConversationDestination as dt, effectiveTaskOutcomes as et, putMcpStoredOAuthCredentials as f, decodeInputImages as ft, readPublicArtifact as g, storeAttachment as gt, publicArtifactHeaders as h, requestAttachmentDeletion as ht, buildAuthPauseResponse as i, acceptsWebMessages as it, parseSlackThreadId as j, RetryableDeliveryError as jt, runWithSlackInstallation as k, readUserCustomization as kt, deleteMcpStoredOAuthCredentials as l, recordWebConversationActivity as lt, readStats as m, readLiveAttachment as mt, executeAgentRun as n, deliverReviewItemChange as nt, deleteMcpAuthSession as o, conversationTurnIdForMessage as ot, latestProgressStatus as p, collectAttachmentGarbage as pt, readConversationSourceTask as q, createContextCompactor as r, renderReviewItemMessage as rt, deleteMcpAuthSessionsForUserProvider as s, createAndEnqueueConversation as st, createAgentRunner as t, listOwnedWatches as tt, getMcpAuthSession as u, webActorFromEmail as ut, lookupSlackUserProfile as v, stopConversationSession as vt, SLACK_REVIEW_ITEM_VIEW_ID as w, conversationSubtreeColumns as wt, createSlackReviewItemShow as x, listChildAgents as xt, renderAttachmentText as y, recordSubagentEnded as yt, logScheduledAutomationRunSkipped as z };
|