@vellumai/assistant 0.11.5 → 0.11.6-staging.1
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/AGENTS.md +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import type { ChannelDeliveryResult } from "@vellumai/gateway-client";
|
|
4
|
-
import {
|
|
3
|
+
import type { ChannelDeliveryResult, StreamOp } from "@vellumai/gateway-client";
|
|
4
|
+
import {
|
|
5
|
+
ChannelReplyPayloadSchema,
|
|
6
|
+
StreamOpSchema,
|
|
7
|
+
} from "@vellumai/gateway-client";
|
|
5
8
|
|
|
6
9
|
type DeliverCall = { callbackUrl: string; payload: Record<string, unknown> };
|
|
7
10
|
|
|
@@ -14,8 +17,8 @@ let deliverImpl: (
|
|
|
14
17
|
ts: "stream-ts-1",
|
|
15
18
|
});
|
|
16
19
|
|
|
17
|
-
const sentStreamOps:
|
|
18
|
-
let streamOpImpl: (op:
|
|
20
|
+
const sentStreamOps: StreamOp[] = [];
|
|
21
|
+
let streamOpImpl: (op: StreamOp) => Promise<{
|
|
19
22
|
ok: boolean;
|
|
20
23
|
ts?: string;
|
|
21
24
|
}> = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
@@ -23,8 +26,13 @@ mock.module("../messaging/providers/index.js", () => ({
|
|
|
23
26
|
sendChannelStreamOp: async (
|
|
24
27
|
_callbackUrl: string,
|
|
25
28
|
_chatId: string,
|
|
26
|
-
op:
|
|
29
|
+
op: StreamOp,
|
|
27
30
|
) => {
|
|
31
|
+
// Every op the session emits must be valid on the wire, the same
|
|
32
|
+
// guarantee the reply-payload mock below enforces. A task-only append
|
|
33
|
+
// carrying neither words nor a plan would move nothing, and the
|
|
34
|
+
// refinement is what says so.
|
|
35
|
+
StreamOpSchema.parse(op);
|
|
28
36
|
sentStreamOps.push(op);
|
|
29
37
|
return streamOpImpl(op);
|
|
30
38
|
},
|
|
@@ -37,7 +45,7 @@ mock.module("./gateway-client.js", () => ({
|
|
|
37
45
|
) => {
|
|
38
46
|
// Every payload the session emits must be valid on the wire — in
|
|
39
47
|
// particular a task-only append must satisfy the contract's
|
|
40
|
-
//
|
|
48
|
+
// text-or-appended-or-plan refinement.
|
|
41
49
|
ChannelReplyPayloadSchema.parse(payload);
|
|
42
50
|
deliverCalls.push({ callbackUrl, payload });
|
|
43
51
|
return deliverImpl(callbackUrl, payload);
|
|
@@ -78,45 +86,72 @@ const messageComplete = (messageId: string): AssistantEvent =>
|
|
|
78
86
|
messageId,
|
|
79
87
|
}) as AssistantEvent;
|
|
80
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The model drawing a plan: a `ui_show` tool call on the turn's own stream,
|
|
91
|
+
* which is how a plan reaches a channel.
|
|
92
|
+
*
|
|
93
|
+
* Built as the tool call rather than as a `ui_surface_show` event, because the
|
|
94
|
+
* event goes to the conversation sink and no channel consumes that. A helper
|
|
95
|
+
* that emitted the event would exercise a path production never takes.
|
|
96
|
+
*/
|
|
81
97
|
const taskProgressShow = (
|
|
82
|
-
surfaceId: string,
|
|
83
98
|
steps: Array<{ label: string; status: string; detail?: string }>,
|
|
84
99
|
templateTitle?: string,
|
|
85
100
|
): AssistantEvent =>
|
|
86
101
|
({
|
|
87
|
-
type: "
|
|
102
|
+
type: "tool_use_start",
|
|
103
|
+
toolName: "ui_show",
|
|
88
104
|
conversationId: "conv-stream",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
toolUseId: "tool-ui-show",
|
|
106
|
+
input: {
|
|
107
|
+
surface_type: "card",
|
|
92
108
|
title: "Task progress",
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
data: {
|
|
110
|
+
template: "task_progress",
|
|
111
|
+
templateData: {
|
|
112
|
+
...(templateTitle ? { title: templateTitle } : {}),
|
|
113
|
+
steps,
|
|
114
|
+
},
|
|
97
115
|
},
|
|
98
116
|
},
|
|
99
117
|
}) as AssistantEvent;
|
|
100
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The successful result of the tool call above. A plan is only applied once its
|
|
121
|
+
* call reports success, so a test that shows a plan without one asserts that
|
|
122
|
+
* nothing renders.
|
|
123
|
+
*/
|
|
124
|
+
const toolOk = (toolUseId: string, toolName: string): AssistantEvent =>
|
|
125
|
+
({
|
|
126
|
+
type: "tool_result",
|
|
127
|
+
toolName,
|
|
128
|
+
result: "ok",
|
|
129
|
+
conversationId: "conv-stream",
|
|
130
|
+
toolUseId,
|
|
131
|
+
}) as AssistantEvent;
|
|
132
|
+
|
|
101
133
|
const taskProgressUpdate = (
|
|
102
|
-
surfaceId: string,
|
|
103
134
|
steps: Array<{ label: string; status: string }>,
|
|
104
135
|
): AssistantEvent =>
|
|
105
136
|
({
|
|
106
|
-
type: "
|
|
137
|
+
type: "tool_use_start",
|
|
138
|
+
toolName: "ui_update",
|
|
107
139
|
conversationId: "conv-stream",
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
toolUseId: "tool-ui-update",
|
|
141
|
+
input: {
|
|
142
|
+
surface_id: "surface-1",
|
|
143
|
+
data: { templateData: { steps } },
|
|
144
|
+
},
|
|
110
145
|
}) as AssistantEvent;
|
|
111
146
|
|
|
112
147
|
const tick = (ms: number): Promise<void> =>
|
|
113
148
|
new Promise((resolve) => setTimeout(resolve, ms));
|
|
114
149
|
|
|
115
|
-
const slackStreamOps = ():
|
|
150
|
+
const slackStreamOps = (): StreamOp[] => sentStreamOps;
|
|
116
151
|
|
|
117
152
|
const streamedMarkdown = (): string =>
|
|
118
153
|
slackStreamOps()
|
|
119
|
-
.map((op) =>
|
|
154
|
+
.map((op) => op.appended ?? "")
|
|
120
155
|
.join("");
|
|
121
156
|
|
|
122
157
|
beforeEach(() => {
|
|
@@ -231,11 +266,15 @@ describe("createSlackReplySession", () => {
|
|
|
231
266
|
expect(slackStreamOps()).toEqual([
|
|
232
267
|
{
|
|
233
268
|
action: "start",
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
269
|
+
anchorMessageId: THREAD_TS,
|
|
270
|
+
text: "The complete answer.",
|
|
271
|
+
appended: "The complete answer.",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
action: "stop",
|
|
275
|
+
streamId: "stream-ts-1",
|
|
276
|
+
text: "The complete answer.",
|
|
237
277
|
},
|
|
238
|
-
{ action: "stop", streamTs: "stream-ts-1" },
|
|
239
278
|
]);
|
|
240
279
|
expect(reconciliation).toEqual({
|
|
241
280
|
mode: "streamed",
|
|
@@ -262,13 +301,16 @@ describe("createSlackReplySession", () => {
|
|
|
262
301
|
expect(slackStreamOps()).toEqual([
|
|
263
302
|
{
|
|
264
303
|
action: "start",
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
anchorMessageId: THREAD_TS,
|
|
305
|
+
text: "The complete answer.",
|
|
306
|
+
appended: "The complete answer.",
|
|
307
|
+
audience: { kind: "oneReader", userId: "U123", userOrgId: "T123" },
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
action: "stop",
|
|
311
|
+
streamId: "stream-ts-1",
|
|
312
|
+
text: "The complete answer.",
|
|
270
313
|
},
|
|
271
|
-
{ action: "stop", streamTs: "stream-ts-1" },
|
|
272
314
|
]);
|
|
273
315
|
});
|
|
274
316
|
|
|
@@ -290,16 +332,21 @@ describe("createSlackReplySession", () => {
|
|
|
290
332
|
expect(slackStreamOps()).toEqual([
|
|
291
333
|
{
|
|
292
334
|
action: "start",
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
335
|
+
anchorMessageId: THREAD_TS,
|
|
336
|
+
text: "First half. ",
|
|
337
|
+
appended: "First half. ",
|
|
296
338
|
},
|
|
297
339
|
{
|
|
298
340
|
action: "append",
|
|
299
|
-
|
|
300
|
-
|
|
341
|
+
streamId: "stream-ts-1",
|
|
342
|
+
text: "First half. Second half.",
|
|
343
|
+
appended: "Second half.",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
action: "stop",
|
|
347
|
+
streamId: "stream-ts-1",
|
|
348
|
+
text: "First half. Second half.",
|
|
301
349
|
},
|
|
302
|
-
{ action: "stop", streamTs: "stream-ts-1" },
|
|
303
350
|
]);
|
|
304
351
|
expect(reconciliation.mode).toBe("streamed");
|
|
305
352
|
});
|
|
@@ -319,12 +366,9 @@ describe("createSlackReplySession", () => {
|
|
|
319
366
|
|
|
320
367
|
const ops = slackStreamOps();
|
|
321
368
|
expect(ops.map((op) => op.action)).toEqual(["start", "append", "stop"]);
|
|
322
|
-
expect((ops[0]
|
|
323
|
-
|
|
324
|
-
);
|
|
325
|
-
expect((ops[1].markdownText as string).length).toBe(4_000);
|
|
326
|
-
const streamed =
|
|
327
|
-
(ops[0].markdownText as string) + (ops[1].markdownText as string);
|
|
369
|
+
expect((ops[0]!.appended ?? "").length).toBe(SLACK_STREAM_MARKDOWN_LIMIT);
|
|
370
|
+
expect((ops[1]!.appended ?? "").length).toBe(4_000);
|
|
371
|
+
const streamed = (ops[0]!.appended ?? "") + (ops[1]!.appended ?? "");
|
|
328
372
|
expect(streamed).toBe(body);
|
|
329
373
|
});
|
|
330
374
|
|
|
@@ -431,39 +475,49 @@ describe("createSlackReplySession", () => {
|
|
|
431
475
|
})!;
|
|
432
476
|
|
|
433
477
|
session.observeEvent(
|
|
434
|
-
taskProgressShow(
|
|
478
|
+
taskProgressShow([
|
|
435
479
|
{ label: "Search docs", status: "in_progress" },
|
|
436
480
|
{ label: "Summarize", status: "pending" },
|
|
437
481
|
]),
|
|
438
482
|
);
|
|
483
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
439
484
|
session.observeEvent(textDelta("Working on it."));
|
|
440
485
|
await tick(15);
|
|
441
486
|
session.observeEvent(
|
|
442
|
-
taskProgressUpdate(
|
|
487
|
+
taskProgressUpdate([
|
|
443
488
|
{ label: "Search docs", status: "completed" },
|
|
444
489
|
{ label: "Summarize", status: "in_progress" },
|
|
445
490
|
]),
|
|
446
491
|
);
|
|
492
|
+
session.observeEvent(toolOk("tool-ui-update", "ui_update"));
|
|
447
493
|
await tick(15);
|
|
448
494
|
|
|
449
495
|
expect(slackStreamOps().at(-1)).toEqual({
|
|
450
496
|
action: "append",
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
497
|
+
streamId: "stream-ts-1",
|
|
498
|
+
// No `appended`: the plan moved and the words did not. `text` is still
|
|
499
|
+
// the whole reply, which is what a channel that rewrites needs.
|
|
500
|
+
text: "Working on it.",
|
|
501
|
+
plan: {
|
|
502
|
+
steps: [
|
|
503
|
+
{ label: "Search docs", status: "completed" },
|
|
504
|
+
{ label: "Summarize", status: "in_progress" },
|
|
505
|
+
],
|
|
506
|
+
},
|
|
456
507
|
});
|
|
457
508
|
|
|
458
509
|
await session.finish();
|
|
459
510
|
|
|
460
511
|
expect(slackStreamOps().at(-1)).toEqual({
|
|
461
512
|
action: "stop",
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
513
|
+
streamId: "stream-ts-1",
|
|
514
|
+
text: "Working on it.",
|
|
515
|
+
plan: {
|
|
516
|
+
steps: [
|
|
517
|
+
{ label: "Search docs", status: "completed" },
|
|
518
|
+
{ label: "Summarize", status: "in_progress" },
|
|
519
|
+
],
|
|
520
|
+
},
|
|
467
521
|
});
|
|
468
522
|
});
|
|
469
523
|
|
|
@@ -477,17 +531,15 @@ describe("createSlackReplySession", () => {
|
|
|
477
531
|
})!;
|
|
478
532
|
|
|
479
533
|
session.observeEvent(
|
|
480
|
-
taskProgressShow("
|
|
481
|
-
{ label: "Search docs", status: "in_progress" },
|
|
482
|
-
]),
|
|
534
|
+
taskProgressShow([{ label: "Search docs", status: "in_progress" }]),
|
|
483
535
|
);
|
|
536
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
484
537
|
session.observeEvent(textDelta("Working on it."));
|
|
485
538
|
await tick(15);
|
|
486
539
|
session.observeEvent(
|
|
487
|
-
taskProgressUpdate("
|
|
488
|
-
{ label: "Search docs", status: "in_progress" },
|
|
489
|
-
]),
|
|
540
|
+
taskProgressUpdate([{ label: "Search docs", status: "in_progress" }]),
|
|
490
541
|
);
|
|
542
|
+
session.observeEvent(toolOk("tool-ui-update", "ui_update"));
|
|
491
543
|
await tick(15);
|
|
492
544
|
|
|
493
545
|
// The start already delivered this exact plan state; a matching update
|
|
@@ -495,9 +547,53 @@ describe("createSlackReplySession", () => {
|
|
|
495
547
|
expect(slackStreamOps().map((op) => op.action)).toEqual(["start"]);
|
|
496
548
|
});
|
|
497
549
|
|
|
550
|
+
test("does not advance a plan the surface tool rejected", async () => {
|
|
551
|
+
const session = createSlackReplySession({
|
|
552
|
+
sourceChannel: "slack",
|
|
553
|
+
chatType: "im",
|
|
554
|
+
replyCallbackUrl: CALLBACK_URL,
|
|
555
|
+
chatId: CHANNEL,
|
|
556
|
+
coalesceMs: 5,
|
|
557
|
+
})!;
|
|
558
|
+
|
|
559
|
+
session.observeEvent(
|
|
560
|
+
taskProgressShow([{ label: "Search docs", status: "in_progress" }]),
|
|
561
|
+
);
|
|
562
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
563
|
+
session.observeEvent(textDelta("Working on it."));
|
|
564
|
+
await tick(15);
|
|
565
|
+
|
|
566
|
+
// A stale surface id or a malformed payload is rejected by the surface
|
|
567
|
+
// tool. The call is an intention, so a plan applied on sight would show a
|
|
568
|
+
// step completed that the canonical surface never accepted.
|
|
569
|
+
session.observeEvent(
|
|
570
|
+
taskProgressUpdate([{ label: "Search docs", status: "completed" }]),
|
|
571
|
+
);
|
|
572
|
+
session.observeEvent({
|
|
573
|
+
type: "tool_result",
|
|
574
|
+
toolName: "ui_update",
|
|
575
|
+
result: "Error: unknown surface_id",
|
|
576
|
+
isError: true,
|
|
577
|
+
conversationId: "conv-stream",
|
|
578
|
+
toolUseId: "tool-ui-update",
|
|
579
|
+
} as AssistantEvent);
|
|
580
|
+
await tick(15);
|
|
581
|
+
await session.finish();
|
|
582
|
+
|
|
583
|
+
// Reads the plan in the assistant's own vocabulary. While the op carried
|
|
584
|
+
// Slack task cards this assertion could not fail: their word for the
|
|
585
|
+
// status is "complete", so "completed" was never going to appear whatever
|
|
586
|
+
// the session did with the rejected update.
|
|
587
|
+
const statuses = slackStreamOps()
|
|
588
|
+
.flatMap((op) => op.plan?.steps ?? [])
|
|
589
|
+
.map((step) => step.status);
|
|
590
|
+
expect(statuses).not.toContain("completed");
|
|
591
|
+
expect(statuses).toContain("in_progress");
|
|
592
|
+
});
|
|
593
|
+
|
|
498
594
|
test("leaves progress to stop when the task-only append fails", async () => {
|
|
499
595
|
streamOpImpl = async (op) => {
|
|
500
|
-
if (op.action === "append" && op.
|
|
596
|
+
if (op.action === "append" && op.appended === undefined) {
|
|
501
597
|
throw new Error("chunks-only append rejected");
|
|
502
598
|
}
|
|
503
599
|
return { ok: true, ts: "stream-ts-1" };
|
|
@@ -513,23 +609,21 @@ describe("createSlackReplySession", () => {
|
|
|
513
609
|
session.observeEvent(textDelta("Working on it."));
|
|
514
610
|
await tick(15);
|
|
515
611
|
session.observeEvent(
|
|
516
|
-
taskProgressShow("
|
|
517
|
-
{ label: "Search docs", status: "in_progress" },
|
|
518
|
-
]),
|
|
612
|
+
taskProgressShow([{ label: "Search docs", status: "in_progress" }]),
|
|
519
613
|
);
|
|
614
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
520
615
|
await tick(15);
|
|
521
616
|
session.observeEvent(
|
|
522
|
-
taskProgressUpdate("
|
|
523
|
-
{ label: "Search docs", status: "completed" },
|
|
524
|
-
]),
|
|
617
|
+
taskProgressUpdate([{ label: "Search docs", status: "completed" }]),
|
|
525
618
|
);
|
|
619
|
+
session.observeEvent(toolOk("tool-ui-update", "ui_update"));
|
|
526
620
|
await tick(15);
|
|
527
621
|
const reconciliation = await session.finish();
|
|
528
622
|
|
|
529
623
|
// The first rejection disables task-only appends for the session, so
|
|
530
624
|
// later progress updates do not retry a doomed call.
|
|
531
625
|
const taskOnlyAttempts = slackStreamOps().filter(
|
|
532
|
-
(op) => op.action === "append" && op.
|
|
626
|
+
(op) => op.action === "append" && op.appended === undefined,
|
|
533
627
|
);
|
|
534
628
|
expect(taskOnlyAttempts.length).toBe(1);
|
|
535
629
|
|
|
@@ -538,8 +632,9 @@ describe("createSlackReplySession", () => {
|
|
|
538
632
|
expect(reconciliation.mode).toBe("streamed");
|
|
539
633
|
expect(slackStreamOps().at(-1)).toEqual({
|
|
540
634
|
action: "stop",
|
|
541
|
-
|
|
542
|
-
|
|
635
|
+
streamId: "stream-ts-1",
|
|
636
|
+
text: "Working on it.",
|
|
637
|
+
plan: { steps: [{ label: "Search docs", status: "completed" }] },
|
|
543
638
|
});
|
|
544
639
|
});
|
|
545
640
|
|
|
@@ -613,9 +708,7 @@ describe("createSlackReplySession", () => {
|
|
|
613
708
|
await session.finish();
|
|
614
709
|
|
|
615
710
|
const ops = slackStreamOps();
|
|
616
|
-
const streamed = ops
|
|
617
|
-
.map((op) => (op.markdownText as string | undefined) ?? "")
|
|
618
|
-
.join("");
|
|
711
|
+
const streamed = ops.map((op) => op.appended ?? "").join("");
|
|
619
712
|
expect(streamed).toBe("Here is your file: report.pdf");
|
|
620
713
|
expect(streamed).not.toContain("vellum://");
|
|
621
714
|
});
|
|
@@ -727,40 +820,47 @@ describe("createSlackReplySession", () => {
|
|
|
727
820
|
})!;
|
|
728
821
|
|
|
729
822
|
session.observeEvent(
|
|
730
|
-
taskProgressShow(
|
|
823
|
+
taskProgressShow([
|
|
731
824
|
{ label: "Search docs", status: "in_progress" },
|
|
732
825
|
{ label: "Summarize", status: "pending" },
|
|
733
826
|
]),
|
|
734
827
|
);
|
|
828
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
735
829
|
session.observeEvent(textDelta("Working on it."));
|
|
736
830
|
await tick(15);
|
|
737
831
|
session.observeEvent(
|
|
738
|
-
taskProgressUpdate(
|
|
832
|
+
taskProgressUpdate([
|
|
739
833
|
{ label: "Search docs", status: "completed" },
|
|
740
834
|
{ label: "Summarize", status: "in_progress" },
|
|
741
835
|
]),
|
|
742
836
|
);
|
|
837
|
+
session.observeEvent(toolOk("tool-ui-update", "ui_update"));
|
|
743
838
|
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
744
839
|
const reconciliation = await session.finish();
|
|
745
840
|
|
|
746
841
|
const ops = slackStreamOps();
|
|
747
842
|
expect(ops[0]).toEqual({
|
|
748
843
|
action: "start",
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
844
|
+
anchorMessageId: THREAD_TS,
|
|
845
|
+
text: "Working on it.",
|
|
846
|
+
appended: "Working on it.",
|
|
847
|
+
plan: {
|
|
848
|
+
steps: [
|
|
849
|
+
{ label: "Search docs", status: "in_progress" },
|
|
850
|
+
{ label: "Summarize", status: "pending" },
|
|
851
|
+
],
|
|
852
|
+
},
|
|
756
853
|
});
|
|
757
854
|
expect(ops.at(-1)).toEqual({
|
|
758
855
|
action: "stop",
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
856
|
+
streamId: "stream-ts-1",
|
|
857
|
+
text: "Working on it.",
|
|
858
|
+
plan: {
|
|
859
|
+
steps: [
|
|
860
|
+
{ label: "Search docs", status: "completed" },
|
|
861
|
+
{ label: "Summarize", status: "in_progress" },
|
|
862
|
+
],
|
|
863
|
+
},
|
|
764
864
|
});
|
|
765
865
|
expect(reconciliation.mode).toBe("streamed");
|
|
766
866
|
});
|
|
@@ -779,14 +879,15 @@ describe("createSlackReplySession", () => {
|
|
|
779
879
|
coalesceMs: 5,
|
|
780
880
|
})!;
|
|
781
881
|
|
|
782
|
-
session.observeEvent(textDelta("On it
|
|
882
|
+
session.observeEvent(textDelta("On it, starting now."));
|
|
783
883
|
await tick(15);
|
|
784
884
|
session.observeEvent(
|
|
785
|
-
taskProgressShow(
|
|
885
|
+
taskProgressShow([
|
|
786
886
|
{ label: "Search docs", status: "in_progress" },
|
|
787
887
|
{ label: "Summarize", status: "pending" },
|
|
788
888
|
]),
|
|
789
889
|
);
|
|
890
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
790
891
|
await tick(15);
|
|
791
892
|
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
792
893
|
await session.finish();
|
|
@@ -794,25 +895,31 @@ describe("createSlackReplySession", () => {
|
|
|
794
895
|
expect(slackStreamOps()).toEqual([
|
|
795
896
|
{
|
|
796
897
|
action: "start",
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
898
|
+
anchorMessageId: THREAD_TS,
|
|
899
|
+
text: "On it, starting now.",
|
|
900
|
+
appended: "On it, starting now.",
|
|
800
901
|
},
|
|
801
902
|
{
|
|
802
903
|
action: "append",
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
904
|
+
streamId: "stream-ts-1",
|
|
905
|
+
text: "On it, starting now.",
|
|
906
|
+
plan: {
|
|
907
|
+
steps: [
|
|
908
|
+
{ label: "Search docs", status: "in_progress" },
|
|
909
|
+
{ label: "Summarize", status: "pending" },
|
|
910
|
+
],
|
|
911
|
+
},
|
|
808
912
|
},
|
|
809
913
|
{
|
|
810
914
|
action: "stop",
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
915
|
+
streamId: "stream-ts-1",
|
|
916
|
+
text: "On it, starting now.",
|
|
917
|
+
plan: {
|
|
918
|
+
steps: [
|
|
919
|
+
{ label: "Search docs", status: "in_progress" },
|
|
920
|
+
{ label: "Summarize", status: "pending" },
|
|
921
|
+
],
|
|
922
|
+
},
|
|
816
923
|
},
|
|
817
924
|
]);
|
|
818
925
|
});
|
|
@@ -828,7 +935,6 @@ describe("createSlackReplySession", () => {
|
|
|
828
935
|
|
|
829
936
|
session.observeEvent(
|
|
830
937
|
taskProgressShow(
|
|
831
|
-
"surface-1",
|
|
832
938
|
[
|
|
833
939
|
{
|
|
834
940
|
label: "Check weather",
|
|
@@ -840,6 +946,7 @@ describe("createSlackReplySession", () => {
|
|
|
840
946
|
"Quick Briefing",
|
|
841
947
|
),
|
|
842
948
|
);
|
|
949
|
+
session.observeEvent(toolOk("tool-ui-show", "ui_show"));
|
|
843
950
|
session.observeEvent(textDelta("Working on it."));
|
|
844
951
|
await tick(15);
|
|
845
952
|
session.observeEvent(messageComplete("assistant-msg-1"));
|
|
@@ -848,23 +955,25 @@ describe("createSlackReplySession", () => {
|
|
|
848
955
|
const ops = slackStreamOps();
|
|
849
956
|
expect(ops[0]).toEqual({
|
|
850
957
|
action: "start",
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
expect(ops.at(-1)).toMatchObject({
|
|
866
|
-
action: "stop",
|
|
867
|
-
planTitle: "Quick Briefing",
|
|
958
|
+
anchorMessageId: THREAD_TS,
|
|
959
|
+
text: "Working on it.",
|
|
960
|
+
appended: "Working on it.",
|
|
961
|
+
plan: {
|
|
962
|
+
title: "Quick Briefing",
|
|
963
|
+
steps: [
|
|
964
|
+
{
|
|
965
|
+
label: "Check weather",
|
|
966
|
+
status: "in_progress",
|
|
967
|
+
detail: "Fetching the forecast",
|
|
968
|
+
},
|
|
969
|
+
{ label: "Summarize", status: "pending" },
|
|
970
|
+
],
|
|
971
|
+
},
|
|
868
972
|
});
|
|
973
|
+
// The plan title survives to the final stop, so a reader who only sees
|
|
974
|
+
// the finished message still sees what the plan was called.
|
|
975
|
+
const final = ops.at(-1);
|
|
976
|
+
expect(final?.action).toBe("stop");
|
|
977
|
+
expect(final?.plan?.title).toBe("Quick Briefing");
|
|
869
978
|
});
|
|
870
979
|
});
|