@vellumai/assistant 0.11.8 → 0.11.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/gemini-provider.test.ts +46 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +19 -6
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
- package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +15 -9
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +47 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -22,7 +22,22 @@ let streamOpImpl: (op: StreamOp) => Promise<{
|
|
|
22
22
|
ok: boolean;
|
|
23
23
|
ts?: string;
|
|
24
24
|
}> = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
25
|
+
/**
|
|
26
|
+
* What the transport lookup reports for the callback under test. A channel
|
|
27
|
+
* declares it can stream by implementing the method, so `undefined` here is a
|
|
28
|
+
* channel that cannot, and `streamPersists` is what says whether the stream
|
|
29
|
+
* leaves the reply behind.
|
|
30
|
+
*/
|
|
31
|
+
let transportImpl:
|
|
32
|
+
| {
|
|
33
|
+
streamReply?: unknown;
|
|
34
|
+
streamPersists?: boolean;
|
|
35
|
+
maxStreamTextChars?: number;
|
|
36
|
+
}
|
|
37
|
+
| undefined = { streamReply: () => undefined, streamPersists: true };
|
|
38
|
+
|
|
25
39
|
mock.module("../messaging/providers/index.js", () => ({
|
|
40
|
+
getTransportForCallback: () => transportImpl,
|
|
26
41
|
sendChannelStreamOp: async (
|
|
27
42
|
_callbackUrl: string,
|
|
28
43
|
_chatId: string,
|
|
@@ -53,11 +68,10 @@ mock.module("./gateway-client.js", () => ({
|
|
|
53
68
|
}));
|
|
54
69
|
|
|
55
70
|
import type { AssistantEvent } from "../api/index.js";
|
|
56
|
-
import { SLACK_STREAM_MARKDOWN_LIMIT } from "../messaging/providers/slack/api.js";
|
|
57
71
|
import {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} from "./
|
|
72
|
+
channelCanStreamReply,
|
|
73
|
+
createChannelReplySession,
|
|
74
|
+
} from "./channel-reply-session.js";
|
|
61
75
|
|
|
62
76
|
const CHANNEL = "D-STREAM";
|
|
63
77
|
const THREAD_TS = "1700000000.000001";
|
|
@@ -155,105 +169,122 @@ const streamedMarkdown = (): string =>
|
|
|
155
169
|
.join("");
|
|
156
170
|
|
|
157
171
|
beforeEach(() => {
|
|
172
|
+
// A channel that streams and whose stream is the reply, which is what the
|
|
173
|
+
// suite below assumes unless a test says otherwise.
|
|
174
|
+
transportImpl = { streamReply: () => undefined, streamPersists: true };
|
|
158
175
|
deliverCalls.length = 0;
|
|
159
176
|
sentStreamOps.length = 0;
|
|
160
177
|
streamOpImpl = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
161
178
|
deliverImpl = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
162
179
|
});
|
|
163
180
|
|
|
164
|
-
describe("
|
|
165
|
-
test("
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
sourceChannel: "slack",
|
|
169
|
-
chatType: "im",
|
|
170
|
-
replyCallbackUrl: CALLBACK_URL,
|
|
171
|
-
}),
|
|
172
|
-
).toBe(true);
|
|
181
|
+
describe("channelCanStreamReply", () => {
|
|
182
|
+
test("a channel whose transport implements streamReply can stream", () => {
|
|
183
|
+
transportImpl = { streamReply: () => undefined, streamPersists: true };
|
|
184
|
+
expect(channelCanStreamReply(CALLBACK_URL)).toBe(true);
|
|
173
185
|
});
|
|
174
186
|
|
|
175
|
-
test("
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
replyCallbackUrl: `https://example.test/deliver/slack?channel=${CHANNEL}`,
|
|
181
|
-
}),
|
|
182
|
-
).toBe(false);
|
|
187
|
+
test("a channel whose transport omits streamReply cannot", () => {
|
|
188
|
+
// Omitting the method is how a channel with no such primitive declares
|
|
189
|
+
// it, so there is nothing else to consult.
|
|
190
|
+
transportImpl = { streamPersists: false };
|
|
191
|
+
expect(channelCanStreamReply(CALLBACK_URL)).toBe(false);
|
|
183
192
|
});
|
|
184
193
|
|
|
185
|
-
test("
|
|
186
|
-
expect(
|
|
187
|
-
shouldStreamSlackReply({
|
|
188
|
-
sourceChannel: "slack",
|
|
189
|
-
chatType: "channel",
|
|
190
|
-
replyCallbackUrl: CALLBACK_URL,
|
|
191
|
-
recipientUserId: "U123",
|
|
192
|
-
recipientTeamId: "T123",
|
|
193
|
-
}),
|
|
194
|
-
).toBe(true);
|
|
194
|
+
test("no callback URL is no channel to ask", () => {
|
|
195
|
+
expect(channelCanStreamReply(undefined)).toBe(false);
|
|
195
196
|
});
|
|
196
197
|
|
|
197
|
-
test("
|
|
198
|
+
test("a session is not created for a channel that cannot stream", () => {
|
|
199
|
+
transportImpl = undefined;
|
|
198
200
|
expect(
|
|
199
|
-
|
|
200
|
-
sourceChannel: "slack",
|
|
201
|
+
createChannelReplySession({
|
|
201
202
|
replyCallbackUrl: CALLBACK_URL,
|
|
202
|
-
|
|
203
|
-
recipientTeamId: "T123",
|
|
203
|
+
chatId: CHANNEL,
|
|
204
204
|
}),
|
|
205
|
-
).
|
|
205
|
+
).toBeUndefined();
|
|
206
206
|
});
|
|
207
|
+
});
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
209
|
+
describe("createChannelReplySession", () => {
|
|
210
|
+
test("a start the channel refuses falls back without streaming", async () => {
|
|
211
|
+
// Whether THIS conversation can carry a growing reply is the platform's
|
|
212
|
+
// rule, answered by the transport's own reply to `start`. Slack refusing
|
|
213
|
+
// a turn with no thread, and Telegram refusing a chat that is not
|
|
214
|
+
// private, both arrive here as the same not-ok.
|
|
215
|
+
streamOpImpl = async () => ({ ok: false });
|
|
216
|
+
const session = createChannelReplySession({
|
|
217
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
218
|
+
chatId: CHANNEL,
|
|
219
|
+
})!;
|
|
220
|
+
|
|
221
|
+
session.observeEvent(textDelta("The complete answer."));
|
|
222
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
223
|
+
const reconciliation = await session.finish();
|
|
224
|
+
|
|
225
|
+
expect(slackStreamOps().map((op) => op.action)).toEqual(["start"]);
|
|
226
|
+
expect(reconciliation).toEqual({ mode: "fallback" });
|
|
217
227
|
});
|
|
218
228
|
|
|
219
|
-
test("
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
229
|
+
test("a preview stream leaves the reply still owed", async () => {
|
|
230
|
+
// Telegram's draft evaporates rather than becoming the reply, so a
|
|
231
|
+
// channel that does not persist its stream reports `fallback`: durable
|
|
232
|
+
// delivery still sends the reply, exactly as for a channel that never
|
|
233
|
+
// streamed. Reporting `streamed` here would drop the reply entirely.
|
|
234
|
+
transportImpl = { streamReply: () => undefined, streamPersists: false };
|
|
235
|
+
const session = createChannelReplySession({
|
|
236
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
237
|
+
chatId: CHANNEL,
|
|
238
|
+
})!;
|
|
239
|
+
|
|
240
|
+
session.observeEvent(textDelta("The complete answer."));
|
|
241
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
242
|
+
const reconciliation = await session.finish();
|
|
243
|
+
|
|
244
|
+
// The preview really was streamed...
|
|
245
|
+
expect(slackStreamOps().map((op) => op.action)).toEqual(["start", "stop"]);
|
|
246
|
+
// ...and the reply is still owed.
|
|
247
|
+
expect(reconciliation).toEqual({ mode: "fallback" });
|
|
228
248
|
});
|
|
229
249
|
|
|
230
|
-
test("
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
test("a preview stream never records an id for crash recovery", async () => {
|
|
251
|
+
// The breadcrumb exists so a retry can reconcile against a message the
|
|
252
|
+
// reader can already see. A preview leaves none, and its id names nothing
|
|
253
|
+
// durable, so recording it would send recovery to edit a message that
|
|
254
|
+
// never existed and lose the reply instead of posting it.
|
|
255
|
+
transportImpl = { streamReply: () => undefined, streamPersists: false };
|
|
256
|
+
const opened: string[] = [];
|
|
257
|
+
const session = createChannelReplySession({
|
|
258
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
259
|
+
chatId: CHANNEL,
|
|
260
|
+
onStreamOpen: (ts) => opened.push(ts),
|
|
261
|
+
})!;
|
|
262
|
+
|
|
263
|
+
session.observeEvent(textDelta("The complete answer."));
|
|
264
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
265
|
+
await session.finish();
|
|
266
|
+
|
|
267
|
+
expect(slackStreamOps().map((op) => op.action)).toEqual(["start", "stop"]);
|
|
268
|
+
expect(opened).toEqual([]);
|
|
238
269
|
});
|
|
239
|
-
});
|
|
240
270
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
271
|
+
test("a stream that becomes the reply does record its id", async () => {
|
|
272
|
+
const opened: string[] = [];
|
|
273
|
+
const session = createChannelReplySession({
|
|
274
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
275
|
+
chatId: CHANNEL,
|
|
276
|
+
onStreamOpen: (ts) => opened.push(ts),
|
|
277
|
+
})!;
|
|
278
|
+
|
|
279
|
+
session.observeEvent(textDelta("The complete answer."));
|
|
280
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
281
|
+
await session.finish();
|
|
282
|
+
|
|
283
|
+
expect(opened).toEqual(["stream-ts-1"]);
|
|
251
284
|
});
|
|
252
285
|
|
|
253
286
|
test("streams a fast turn as a single start then stop", async () => {
|
|
254
|
-
const session =
|
|
255
|
-
sourceChannel: "slack",
|
|
256
|
-
chatType: "im",
|
|
287
|
+
const session = createChannelReplySession({
|
|
257
288
|
replyCallbackUrl: CALLBACK_URL,
|
|
258
289
|
chatId: CHANNEL,
|
|
259
290
|
})!;
|
|
@@ -266,7 +297,6 @@ describe("createSlackReplySession", () => {
|
|
|
266
297
|
expect(slackStreamOps()).toEqual([
|
|
267
298
|
{
|
|
268
299
|
action: "start",
|
|
269
|
-
anchorMessageId: THREAD_TS,
|
|
270
300
|
text: "The complete answer.",
|
|
271
301
|
appended: "The complete answer.",
|
|
272
302
|
},
|
|
@@ -284,9 +314,7 @@ describe("createSlackReplySession", () => {
|
|
|
284
314
|
});
|
|
285
315
|
|
|
286
316
|
test("stamps recipient IDs on the start op for a channel turn", async () => {
|
|
287
|
-
const session =
|
|
288
|
-
sourceChannel: "slack",
|
|
289
|
-
chatType: "channel",
|
|
317
|
+
const session = createChannelReplySession({
|
|
290
318
|
replyCallbackUrl: CALLBACK_URL,
|
|
291
319
|
chatId: CHANNEL,
|
|
292
320
|
recipientUserId: "U123",
|
|
@@ -301,7 +329,6 @@ describe("createSlackReplySession", () => {
|
|
|
301
329
|
expect(slackStreamOps()).toEqual([
|
|
302
330
|
{
|
|
303
331
|
action: "start",
|
|
304
|
-
anchorMessageId: THREAD_TS,
|
|
305
332
|
text: "The complete answer.",
|
|
306
333
|
appended: "The complete answer.",
|
|
307
334
|
audience: { kind: "oneReader", userId: "U123", userOrgId: "T123" },
|
|
@@ -315,9 +342,7 @@ describe("createSlackReplySession", () => {
|
|
|
315
342
|
});
|
|
316
343
|
|
|
317
344
|
test("coalesces mid-stream deltas into incremental appends", async () => {
|
|
318
|
-
const session =
|
|
319
|
-
sourceChannel: "slack",
|
|
320
|
-
chatType: "im",
|
|
345
|
+
const session = createChannelReplySession({
|
|
321
346
|
replyCallbackUrl: CALLBACK_URL,
|
|
322
347
|
chatId: CHANNEL,
|
|
323
348
|
coalesceMs: 5,
|
|
@@ -332,7 +357,6 @@ describe("createSlackReplySession", () => {
|
|
|
332
357
|
expect(slackStreamOps()).toEqual([
|
|
333
358
|
{
|
|
334
359
|
action: "start",
|
|
335
|
-
anchorMessageId: THREAD_TS,
|
|
336
360
|
text: "First half. ",
|
|
337
361
|
appended: "First half. ",
|
|
338
362
|
},
|
|
@@ -351,32 +375,101 @@ describe("createSlackReplySession", () => {
|
|
|
351
375
|
expect(reconciliation.mode).toBe("streamed");
|
|
352
376
|
});
|
|
353
377
|
|
|
354
|
-
test("drains a body wider than the
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
378
|
+
test("drains a body wider than the channel's cap, one operation per chunk", async () => {
|
|
379
|
+
transportImpl = {
|
|
380
|
+
streamReply: () => undefined,
|
|
381
|
+
streamPersists: true,
|
|
382
|
+
maxStreamTextChars: 10,
|
|
383
|
+
};
|
|
384
|
+
const session = createChannelReplySession({
|
|
385
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
386
|
+
chatId: CHANNEL,
|
|
387
|
+
})!;
|
|
388
|
+
|
|
389
|
+
const body = "abcdefghijKLMNOPQRSTuvwxy";
|
|
390
|
+
session.observeEvent(textDelta(body));
|
|
391
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
392
|
+
await session.finish();
|
|
393
|
+
|
|
394
|
+
const ops = slackStreamOps();
|
|
395
|
+
expect(ops.map((op) => op.action)).toEqual([
|
|
396
|
+
"start",
|
|
397
|
+
"append",
|
|
398
|
+
"append",
|
|
399
|
+
"stop",
|
|
400
|
+
]);
|
|
401
|
+
// Every chunk fits the cap, and together they are the whole reply.
|
|
402
|
+
expect(ops[0]!.appended).toBe("abcdefghij");
|
|
403
|
+
expect(ops[1]!.appended).toBe("KLMNOPQRST");
|
|
404
|
+
expect(ops[2]!.appended).toBe("uvwxy");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a failed chunk resumes at itself, never at what already landed", async () => {
|
|
408
|
+
// The delivered mark advances once per confirmed operation, so a retry
|
|
409
|
+
// begins at the chunk that failed. Advancing it for a delta the channel
|
|
410
|
+
// only partly took would resume past unsent text, or re-send text the
|
|
411
|
+
// reader can already see.
|
|
412
|
+
transportImpl = {
|
|
413
|
+
streamReply: () => undefined,
|
|
414
|
+
streamPersists: true,
|
|
415
|
+
maxStreamTextChars: 10,
|
|
416
|
+
};
|
|
417
|
+
const session = createChannelReplySession({
|
|
358
418
|
replyCallbackUrl: CALLBACK_URL,
|
|
359
419
|
chatId: CHANNEL,
|
|
420
|
+
coalesceMs: 5,
|
|
360
421
|
})!;
|
|
361
422
|
|
|
362
|
-
|
|
423
|
+
let failNextAppend = true;
|
|
424
|
+
streamOpImpl = async (op) => {
|
|
425
|
+
if (op.action === "append" && failNextAppend) {
|
|
426
|
+
failNextAppend = false;
|
|
427
|
+
throw new Error("Simulated append failure");
|
|
428
|
+
}
|
|
429
|
+
return { ok: true, ts: "stream-ts-1" };
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
session.observeEvent(textDelta("abcdefghijKLMNOPQRST"));
|
|
433
|
+
await tick(15);
|
|
434
|
+
session.observeEvent(textDelta("uvwxy"));
|
|
435
|
+
await tick(15);
|
|
436
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
437
|
+
await session.finish();
|
|
438
|
+
|
|
439
|
+
const appended = slackStreamOps()
|
|
440
|
+
.filter((op) => op.action === "append")
|
|
441
|
+
.map((op) => op.appended);
|
|
442
|
+
// The first attempt at "KLMNOPQRST" fails and is retried; nothing that
|
|
443
|
+
// landed is ever sent a second time.
|
|
444
|
+
expect(appended[0]).toBe("KLMNOPQRST");
|
|
445
|
+
expect(appended[1]).toBe("KLMNOPQRST");
|
|
446
|
+
expect(appended.filter((c) => c === "abcdefghij")).toHaveLength(0);
|
|
447
|
+
expect(appended.at(-1)).toBe("uvwxy");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("hands a body of any width over as one delta", async () => {
|
|
451
|
+
// Splitting belongs to the channel whose API sets the cap, so the session
|
|
452
|
+
// must not pre-split: a channel with a wider cap, or none, would be paying
|
|
453
|
+
// for Slack's. What it owes is the whole delta, once.
|
|
454
|
+
const session = createChannelReplySession({
|
|
455
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
456
|
+
chatId: CHANNEL,
|
|
457
|
+
})!;
|
|
458
|
+
|
|
459
|
+
const body = "x".repeat(20_000);
|
|
363
460
|
session.observeEvent(textDelta(body));
|
|
364
461
|
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
365
462
|
await session.finish();
|
|
366
463
|
|
|
367
464
|
const ops = slackStreamOps();
|
|
368
|
-
expect(ops.map((op) => op.action)).toEqual(["start", "
|
|
369
|
-
expect(
|
|
370
|
-
expect(
|
|
371
|
-
const streamed = (ops[0]!.appended ?? "") + (ops[1]!.appended ?? "");
|
|
372
|
-
expect(streamed).toBe(body);
|
|
465
|
+
expect(ops.map((op) => op.action)).toEqual(["start", "stop"]);
|
|
466
|
+
expect(ops[0]!.appended).toBe(body);
|
|
467
|
+
expect(ops[0]!.text).toBe(body);
|
|
373
468
|
});
|
|
374
469
|
|
|
375
470
|
test("falls back when startStream returns no stream ts", async () => {
|
|
376
471
|
streamOpImpl = async () => ({ ok: false });
|
|
377
|
-
const session =
|
|
378
|
-
sourceChannel: "slack",
|
|
379
|
-
chatType: "im",
|
|
472
|
+
const session = createChannelReplySession({
|
|
380
473
|
replyCallbackUrl: CALLBACK_URL,
|
|
381
474
|
chatId: CHANNEL,
|
|
382
475
|
})!;
|
|
@@ -393,9 +486,7 @@ describe("createSlackReplySession", () => {
|
|
|
393
486
|
streamOpImpl = async () => {
|
|
394
487
|
throw new Error("rate limited");
|
|
395
488
|
};
|
|
396
|
-
const session =
|
|
397
|
-
sourceChannel: "slack",
|
|
398
|
-
chatType: "im",
|
|
489
|
+
const session = createChannelReplySession({
|
|
399
490
|
replyCallbackUrl: CALLBACK_URL,
|
|
400
491
|
chatId: CHANNEL,
|
|
401
492
|
})!;
|
|
@@ -414,9 +505,7 @@ describe("createSlackReplySession", () => {
|
|
|
414
505
|
}
|
|
415
506
|
return { ok: true, ts: "stream-ts-1" };
|
|
416
507
|
};
|
|
417
|
-
const session =
|
|
418
|
-
sourceChannel: "slack",
|
|
419
|
-
chatType: "im",
|
|
508
|
+
const session = createChannelReplySession({
|
|
420
509
|
replyCallbackUrl: CALLBACK_URL,
|
|
421
510
|
chatId: CHANNEL,
|
|
422
511
|
})!;
|
|
@@ -435,9 +524,7 @@ describe("createSlackReplySession", () => {
|
|
|
435
524
|
// transient breadcrumb write error) must not knock the session into
|
|
436
525
|
// fallback — that would repost the already-visible streamed reply.
|
|
437
526
|
const onStreamOpenCalls: string[] = [];
|
|
438
|
-
const session =
|
|
439
|
-
sourceChannel: "slack",
|
|
440
|
-
chatType: "im",
|
|
527
|
+
const session = createChannelReplySession({
|
|
441
528
|
replyCallbackUrl: CALLBACK_URL,
|
|
442
529
|
chatId: CHANNEL,
|
|
443
530
|
onStreamOpen: (streamTs) => {
|
|
@@ -466,9 +553,7 @@ describe("createSlackReplySession", () => {
|
|
|
466
553
|
// `chat.appendStream` accepts a chunks-only call, so a plan that advances
|
|
467
554
|
// during tool work lands live instead of waiting for the final stop.
|
|
468
555
|
// @see https://docs.slack.dev/reference/methods/chat.appendStream/
|
|
469
|
-
const session =
|
|
470
|
-
sourceChannel: "slack",
|
|
471
|
-
chatType: "im",
|
|
556
|
+
const session = createChannelReplySession({
|
|
472
557
|
replyCallbackUrl: CALLBACK_URL,
|
|
473
558
|
chatId: CHANNEL,
|
|
474
559
|
coalesceMs: 5,
|
|
@@ -522,9 +607,7 @@ describe("createSlackReplySession", () => {
|
|
|
522
607
|
});
|
|
523
608
|
|
|
524
609
|
test("does not re-append unchanged task progress", async () => {
|
|
525
|
-
const session =
|
|
526
|
-
sourceChannel: "slack",
|
|
527
|
-
chatType: "im",
|
|
610
|
+
const session = createChannelReplySession({
|
|
528
611
|
replyCallbackUrl: CALLBACK_URL,
|
|
529
612
|
chatId: CHANNEL,
|
|
530
613
|
coalesceMs: 5,
|
|
@@ -548,9 +631,7 @@ describe("createSlackReplySession", () => {
|
|
|
548
631
|
});
|
|
549
632
|
|
|
550
633
|
test("does not advance a plan the surface tool rejected", async () => {
|
|
551
|
-
const session =
|
|
552
|
-
sourceChannel: "slack",
|
|
553
|
-
chatType: "im",
|
|
634
|
+
const session = createChannelReplySession({
|
|
554
635
|
replyCallbackUrl: CALLBACK_URL,
|
|
555
636
|
chatId: CHANNEL,
|
|
556
637
|
coalesceMs: 5,
|
|
@@ -598,9 +679,7 @@ describe("createSlackReplySession", () => {
|
|
|
598
679
|
}
|
|
599
680
|
return { ok: true, ts: "stream-ts-1" };
|
|
600
681
|
};
|
|
601
|
-
const session =
|
|
602
|
-
sourceChannel: "slack",
|
|
603
|
-
chatType: "im",
|
|
682
|
+
const session = createChannelReplySession({
|
|
604
683
|
replyCallbackUrl: CALLBACK_URL,
|
|
605
684
|
chatId: CHANNEL,
|
|
606
685
|
coalesceMs: 5,
|
|
@@ -639,9 +718,7 @@ describe("createSlackReplySession", () => {
|
|
|
639
718
|
});
|
|
640
719
|
|
|
641
720
|
test("never opens a stream for a no_response-only turn", async () => {
|
|
642
|
-
const session =
|
|
643
|
-
sourceChannel: "slack",
|
|
644
|
-
chatType: "im",
|
|
721
|
+
const session = createChannelReplySession({
|
|
645
722
|
replyCallbackUrl: CALLBACK_URL,
|
|
646
723
|
chatId: CHANNEL,
|
|
647
724
|
})!;
|
|
@@ -657,9 +734,7 @@ describe("createSlackReplySession", () => {
|
|
|
657
734
|
test("holds the stream while a no_response sentinel arrives in pieces", async () => {
|
|
658
735
|
// A coalesce timer must not open a stream on the leading `<` of a slowly
|
|
659
736
|
// streamed `<no_response/>`, which would leak a stray partial message.
|
|
660
|
-
const session =
|
|
661
|
-
sourceChannel: "slack",
|
|
662
|
-
chatType: "im",
|
|
737
|
+
const session = createChannelReplySession({
|
|
663
738
|
replyCallbackUrl: CALLBACK_URL,
|
|
664
739
|
chatId: CHANNEL,
|
|
665
740
|
coalesceMs: 5,
|
|
@@ -685,9 +760,7 @@ describe("createSlackReplySession", () => {
|
|
|
685
760
|
// Slack streams are append-only, so a `[label](vellum://…)` link split
|
|
686
761
|
// across deltas must not stream its internal path before the closing `)`
|
|
687
762
|
// arrives — once emitted it could not be retracted.
|
|
688
|
-
const session =
|
|
689
|
-
sourceChannel: "slack",
|
|
690
|
-
chatType: "im",
|
|
763
|
+
const session = createChannelReplySession({
|
|
691
764
|
replyCallbackUrl: CALLBACK_URL,
|
|
692
765
|
chatId: CHANNEL,
|
|
693
766
|
coalesceMs: 5,
|
|
@@ -714,9 +787,7 @@ describe("createSlackReplySession", () => {
|
|
|
714
787
|
});
|
|
715
788
|
|
|
716
789
|
test("counts deliverable text segments split at tool boundaries", async () => {
|
|
717
|
-
const session =
|
|
718
|
-
sourceChannel: "slack",
|
|
719
|
-
chatType: "im",
|
|
790
|
+
const session = createChannelReplySession({
|
|
720
791
|
replyCallbackUrl: CALLBACK_URL,
|
|
721
792
|
chatId: CHANNEL,
|
|
722
793
|
})!;
|
|
@@ -738,9 +809,7 @@ describe("createSlackReplySession", () => {
|
|
|
738
809
|
// The model ends one segment with a period and opens the next with a
|
|
739
810
|
// capital letter, supplying no separating whitespace on either side.
|
|
740
811
|
// Concatenating them raw would fuse "Sentence one.Sentence two.".
|
|
741
|
-
const session =
|
|
742
|
-
sourceChannel: "slack",
|
|
743
|
-
chatType: "im",
|
|
812
|
+
const session = createChannelReplySession({
|
|
744
813
|
replyCallbackUrl: CALLBACK_URL,
|
|
745
814
|
chatId: CHANNEL,
|
|
746
815
|
})!;
|
|
@@ -758,9 +827,7 @@ describe("createSlackReplySession", () => {
|
|
|
758
827
|
// Multiple `message_complete` events fire within one streamed turn (one
|
|
759
828
|
// per model response). Text from the second response must not fuse onto
|
|
760
829
|
// the first.
|
|
761
|
-
const session =
|
|
762
|
-
sourceChannel: "slack",
|
|
763
|
-
chatType: "im",
|
|
830
|
+
const session = createChannelReplySession({
|
|
764
831
|
replyCallbackUrl: CALLBACK_URL,
|
|
765
832
|
chatId: CHANNEL,
|
|
766
833
|
})!;
|
|
@@ -775,9 +842,7 @@ describe("createSlackReplySession", () => {
|
|
|
775
842
|
});
|
|
776
843
|
|
|
777
844
|
test("does not double-space a boundary the model already spaced", async () => {
|
|
778
|
-
const session =
|
|
779
|
-
sourceChannel: "slack",
|
|
780
|
-
chatType: "im",
|
|
845
|
+
const session = createChannelReplySession({
|
|
781
846
|
replyCallbackUrl: CALLBACK_URL,
|
|
782
847
|
chatId: CHANNEL,
|
|
783
848
|
})!;
|
|
@@ -794,9 +859,7 @@ describe("createSlackReplySession", () => {
|
|
|
794
859
|
test("does not fuse mid-word deltas within a single segment", async () => {
|
|
795
860
|
// Intra-segment token deltas carry the model's own spacing and must never
|
|
796
861
|
// be altered — only tool/message boundaries introduce a separating space.
|
|
797
|
-
const session =
|
|
798
|
-
sourceChannel: "slack",
|
|
799
|
-
chatType: "im",
|
|
862
|
+
const session = createChannelReplySession({
|
|
800
863
|
replyCallbackUrl: CALLBACK_URL,
|
|
801
864
|
chatId: CHANNEL,
|
|
802
865
|
})!;
|
|
@@ -811,9 +874,7 @@ describe("createSlackReplySession", () => {
|
|
|
811
874
|
});
|
|
812
875
|
|
|
813
876
|
test("opens the stream in plan mode and advances task cards", async () => {
|
|
814
|
-
const session =
|
|
815
|
-
sourceChannel: "slack",
|
|
816
|
-
chatType: "im",
|
|
877
|
+
const session = createChannelReplySession({
|
|
817
878
|
replyCallbackUrl: CALLBACK_URL,
|
|
818
879
|
chatId: CHANNEL,
|
|
819
880
|
coalesceMs: 5,
|
|
@@ -841,7 +902,6 @@ describe("createSlackReplySession", () => {
|
|
|
841
902
|
const ops = slackStreamOps();
|
|
842
903
|
expect(ops[0]).toEqual({
|
|
843
904
|
action: "start",
|
|
844
|
-
anchorMessageId: THREAD_TS,
|
|
845
905
|
text: "Working on it.",
|
|
846
906
|
appended: "Working on it.",
|
|
847
907
|
plan: {
|
|
@@ -871,9 +931,7 @@ describe("createSlackReplySession", () => {
|
|
|
871
931
|
// stream starts, so the start must open in plan mode even with no plan
|
|
872
932
|
// active yet — otherwise the late-arriving task cards can never render
|
|
873
933
|
// as a plan.
|
|
874
|
-
const session =
|
|
875
|
-
sourceChannel: "slack",
|
|
876
|
-
chatType: "im",
|
|
934
|
+
const session = createChannelReplySession({
|
|
877
935
|
replyCallbackUrl: CALLBACK_URL,
|
|
878
936
|
chatId: CHANNEL,
|
|
879
937
|
coalesceMs: 5,
|
|
@@ -895,7 +953,6 @@ describe("createSlackReplySession", () => {
|
|
|
895
953
|
expect(slackStreamOps()).toEqual([
|
|
896
954
|
{
|
|
897
955
|
action: "start",
|
|
898
|
-
anchorMessageId: THREAD_TS,
|
|
899
956
|
text: "On it, starting now.",
|
|
900
957
|
appended: "On it, starting now.",
|
|
901
958
|
},
|
|
@@ -924,10 +981,92 @@ describe("createSlackReplySession", () => {
|
|
|
924
981
|
]);
|
|
925
982
|
});
|
|
926
983
|
|
|
984
|
+
test("opens the stream on a plan shown before any prose", async () => {
|
|
985
|
+
// What the Slack `ui_show` description actually instructs: show the card
|
|
986
|
+
// early on a multi-step turn. At that moment the model has written no
|
|
987
|
+
// prose, so a stream that waits for text holds the plan back until the
|
|
988
|
+
// final answer, when the steps it narrates are already finished.
|
|
989
|
+
const session = createChannelReplySession({
|
|
990
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
991
|
+
chatId: CHANNEL,
|
|
992
|
+
coalesceMs: 5,
|
|
993
|
+
})!;
|
|
994
|
+
|
|
995
|
+
session.observeEvent(
|
|
996
|
+
taskProgressShow([
|
|
997
|
+
{ label: "Search docs", status: "in_progress" },
|
|
998
|
+
{ label: "Summarize", status: "pending" },
|
|
999
|
+
]),
|
|
1000
|
+
);
|
|
1001
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
1002
|
+
await tick(15);
|
|
1003
|
+
|
|
1004
|
+
// The plan is on Slack before the turn has produced a single character.
|
|
1005
|
+
expect(slackStreamOps()).toEqual([
|
|
1006
|
+
{
|
|
1007
|
+
action: "start",
|
|
1008
|
+
text: "",
|
|
1009
|
+
appended: "",
|
|
1010
|
+
plan: {
|
|
1011
|
+
steps: [
|
|
1012
|
+
{ label: "Search docs", status: "in_progress" },
|
|
1013
|
+
{ label: "Summarize", status: "pending" },
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
]);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test("advances plan steps live while the turn is still working", async () => {
|
|
1021
|
+
const session = createChannelReplySession({
|
|
1022
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
1023
|
+
chatId: CHANNEL,
|
|
1024
|
+
coalesceMs: 5,
|
|
1025
|
+
})!;
|
|
1026
|
+
|
|
1027
|
+
session.observeEvent(
|
|
1028
|
+
taskProgressShow([
|
|
1029
|
+
{ label: "Search docs", status: "in_progress" },
|
|
1030
|
+
{ label: "Summarize", status: "pending" },
|
|
1031
|
+
]),
|
|
1032
|
+
);
|
|
1033
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
1034
|
+
await tick(15);
|
|
1035
|
+
session.observeEvent(
|
|
1036
|
+
taskProgressUpdate([
|
|
1037
|
+
{ label: "Search docs", status: "completed" },
|
|
1038
|
+
{ label: "Summarize", status: "in_progress" },
|
|
1039
|
+
]),
|
|
1040
|
+
);
|
|
1041
|
+
session.observeEvent(toolOk("tool-ui-update", "ui_update"));
|
|
1042
|
+
await tick(15);
|
|
1043
|
+
|
|
1044
|
+
// The advance rides its own task-only append rather than waiting for the
|
|
1045
|
+
// first text, which is the whole point of a live step tracker.
|
|
1046
|
+
const advances = slackStreamOps().filter((op) => op.action === "append");
|
|
1047
|
+
expect(advances).toHaveLength(1);
|
|
1048
|
+
expect(advances[0].plan).toEqual({
|
|
1049
|
+
steps: [
|
|
1050
|
+
{ label: "Search docs", status: "completed" },
|
|
1051
|
+
{ label: "Summarize", status: "in_progress" },
|
|
1052
|
+
],
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
session.observeEvent(textDelta("All done."));
|
|
1056
|
+
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
1057
|
+
const reconciliation = await session.finish();
|
|
1058
|
+
|
|
1059
|
+
// The reply still streams into the same message the plan opened.
|
|
1060
|
+
expect(reconciliation).toEqual({
|
|
1061
|
+
mode: "streamed",
|
|
1062
|
+
messageTs: "stream-ts-1",
|
|
1063
|
+
deliveredSegmentCount: 1,
|
|
1064
|
+
});
|
|
1065
|
+
expect(streamedMarkdown()).toBe("All done.");
|
|
1066
|
+
});
|
|
1067
|
+
|
|
927
1068
|
test("carries the plan title and step details onto stream ops", async () => {
|
|
928
|
-
const session =
|
|
929
|
-
sourceChannel: "slack",
|
|
930
|
-
chatType: "im",
|
|
1069
|
+
const session = createChannelReplySession({
|
|
931
1070
|
replyCallbackUrl: CALLBACK_URL,
|
|
932
1071
|
chatId: CHANNEL,
|
|
933
1072
|
coalesceMs: 5,
|
|
@@ -955,7 +1094,6 @@ describe("createSlackReplySession", () => {
|
|
|
955
1094
|
const ops = slackStreamOps();
|
|
956
1095
|
expect(ops[0]).toEqual({
|
|
957
1096
|
action: "start",
|
|
958
|
-
anchorMessageId: THREAD_TS,
|
|
959
1097
|
text: "Working on it.",
|
|
960
1098
|
appended: "Working on it.",
|
|
961
1099
|
plan: {
|