@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231715.8da971c
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +42 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +97 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +69 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +9 -4
- package/src/calls/voice-escalation-judge.ts +7 -6
- package/src/calls/voice-leg-coordinator.ts +15 -4
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +50 -20
- package/src/calls/voice-triage-escalate.ts +61 -26
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +37 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +17 -14
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +13 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +116 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/conversation-types.ts +6 -2
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -54,6 +54,7 @@ export const WebSearchProviderIdSchema = z.enum([
|
|
|
54
54
|
"fastcrw",
|
|
55
55
|
"searxng",
|
|
56
56
|
"tinyfish",
|
|
57
|
+
"exa",
|
|
57
58
|
]);
|
|
58
59
|
|
|
59
60
|
export type WebSearchProviderId = z.infer<typeof WebSearchProviderIdSchema>;
|
|
@@ -112,6 +113,16 @@ export const WebFetchMetadataSchema = z.object({
|
|
|
112
113
|
durationMs: z.number(),
|
|
113
114
|
errorMessage: z.string().optional(),
|
|
114
115
|
mayRequireJavaScript: z.boolean().optional(),
|
|
116
|
+
/**
|
|
117
|
+
* The requested `start_index` was past the end of the page, so nothing was
|
|
118
|
+
* read. Always set, true or false, by an assistant whose metadata carries
|
|
119
|
+
* every warning a reader acts on (this and `providerWarning` included); a
|
|
120
|
+
* record without it came from one that predates them, whose remaining
|
|
121
|
+
* warnings are only in the result text.
|
|
122
|
+
*/
|
|
123
|
+
startIndexPastEnd: z.boolean().optional(),
|
|
124
|
+
/** A hosted provider's own warning about the fetch, verbatim. */
|
|
125
|
+
providerWarning: z.string().optional(),
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
export type WebFetchMetadata = z.infer<typeof WebFetchMetadataSchema>;
|
package/src/api/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
export {
|
|
4
|
+
type AskQuestionInput,
|
|
5
|
+
AskQuestionInputSchema,
|
|
6
|
+
type RecordedQuestion,
|
|
7
|
+
} from "./ask-question-tool.js";
|
|
3
8
|
export {
|
|
4
9
|
isComputerUseToolCall,
|
|
5
10
|
resolveComputerUseToolName,
|
|
@@ -832,6 +837,8 @@ export {
|
|
|
832
837
|
FeedItemStatusSchema,
|
|
833
838
|
type FeedItemType,
|
|
834
839
|
FeedItemTypeSchema,
|
|
840
|
+
type FeedItemUpdate,
|
|
841
|
+
FeedItemUpdateSchema,
|
|
835
842
|
type FeedItemUrgency,
|
|
836
843
|
FeedItemUrgencySchema,
|
|
837
844
|
GUARDIAN_TERMINAL_REASON_SUPERSEDED,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wire contract for the dictation REST endpoint (`POST /dictation`).
|
|
3
3
|
*
|
|
4
|
-
* The body web/macOS sends
|
|
5
|
-
*
|
|
4
|
+
* The body web/macOS sends to apply explicit dictation replacements, classify
|
|
5
|
+
* voice input, or edit selected text. `context` is its own
|
|
6
6
|
* named schema/type so consumers (web client) can import `DictationContext`
|
|
7
7
|
* directly instead of inferring it from the generated client.
|
|
8
8
|
*
|
|
@@ -167,6 +167,28 @@ export type FeedItemGuardianRequest = z.infer<
|
|
|
167
167
|
typeof FeedItemGuardianRequestSchema
|
|
168
168
|
>;
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* One background rewrite of a managed skill, as a receipt item lists it.
|
|
172
|
+
*
|
|
173
|
+
* A receipt (`detailPanel.kind === "updatesList"`) collapses every skill
|
|
174
|
+
* update a background pass made within one burst into a single feed item, and
|
|
175
|
+
* `updates` keeps each rewrite whole: the skill it touched, the pass's own
|
|
176
|
+
* account of what changed, and the conversation the change came from, when
|
|
177
|
+
* lineage resolved. The same skill can appear more than once, one entry per
|
|
178
|
+
* rewrite, in the order the rewrites happened. Clients group on `skillId`
|
|
179
|
+
* alone, never on `name` or the summary text.
|
|
180
|
+
*/
|
|
181
|
+
export const FeedItemUpdateSchema = z.object({
|
|
182
|
+
skillId: z.string(),
|
|
183
|
+
/** The skill's display name at the time of the rewrite. */
|
|
184
|
+
name: z.string(),
|
|
185
|
+
/** The pass's own account of what it changed. Plain text, not markdown. */
|
|
186
|
+
summary: z.string(),
|
|
187
|
+
/** The conversation the rewrite was distilled from, when it resolved. */
|
|
188
|
+
conversationId: z.string().optional(),
|
|
189
|
+
});
|
|
190
|
+
export type FeedItemUpdate = z.infer<typeof FeedItemUpdateSchema>;
|
|
191
|
+
|
|
170
192
|
/**
|
|
171
193
|
* A single item rendered in the Home feed.
|
|
172
194
|
*
|
|
@@ -198,6 +220,11 @@ export const FeedItemSchema = z.object({
|
|
|
198
220
|
// Present only on the canonical "Needs attention" item projecting a
|
|
199
221
|
// guardian request; see FeedItemGuardianRequestSchema.
|
|
200
222
|
guardianRequest: FeedItemGuardianRequestSchema.optional(),
|
|
223
|
+
// Present only on a skill-update receipt (`detailPanel.kind` is
|
|
224
|
+
// `updatesList`); see FeedItemUpdateSchema. `summary` still carries the
|
|
225
|
+
// same content as a plain list, so a client without the panel loses only
|
|
226
|
+
// the per-skill links.
|
|
227
|
+
updates: z.array(FeedItemUpdateSchema).optional(),
|
|
201
228
|
category: FeedItemCategorySchema.optional(),
|
|
202
229
|
noteworthy: z.boolean().optional(),
|
|
203
230
|
fromAssistant: z.boolean().optional(),
|
|
@@ -63,9 +63,6 @@ const TRAITS = { bodyShape: "blob", eyeStyle: "curious", color: "green" };
|
|
|
63
63
|
const GREEN_DISC = { r: 0xe6, g: 0xf1, b: 0xe7 };
|
|
64
64
|
/** #E9642F mixed 14% into white. */
|
|
65
65
|
const ORANGE_DISC = { r: 0xfc, g: 0xe9, b: 0xe2 };
|
|
66
|
-
/** The neutral disc every accent-less avatar wears. */
|
|
67
|
-
const FALLBACK_DISC = { r: 0xec, g: 0xef, b: 0xea };
|
|
68
|
-
|
|
69
66
|
/** On the disc and clear of the inset avatar square, which starts at y = 28.16. */
|
|
70
67
|
const DISC_SAMPLE = { x: 128, y: 8 };
|
|
71
68
|
/** Outside the inscribed disc, where the render leaves nothing at all. */
|
|
@@ -233,7 +230,7 @@ describe("renderNotificationAvatarPng", () => {
|
|
|
233
230
|
);
|
|
234
231
|
|
|
235
232
|
nativeTest(
|
|
236
|
-
"
|
|
233
|
+
"fills the circle with an uploaded image when it has no accent",
|
|
237
234
|
async () => {
|
|
238
235
|
await writeUpload("png", null);
|
|
239
236
|
|
|
@@ -245,7 +242,9 @@ describe("renderNotificationAvatarPng", () => {
|
|
|
245
242
|
expect(image.height).toBe(256);
|
|
246
243
|
expect(image.at(CORNER.x, CORNER.y).a).toBe(0);
|
|
247
244
|
expect(image.at(DISC_SAMPLE.x, DISC_SAMPLE.y)).toEqual({
|
|
248
|
-
|
|
245
|
+
r: 0x20,
|
|
246
|
+
g: 0x60,
|
|
247
|
+
b: 0xc0,
|
|
249
248
|
a: 255,
|
|
250
249
|
});
|
|
251
250
|
expect(image.at(128, 128)).toEqual({ r: 0x20, g: 0x60, b: 0xc0, a: 255 });
|
|
@@ -253,23 +252,34 @@ describe("renderNotificationAvatarPng", () => {
|
|
|
253
252
|
NATIVE_RENDER_TEST_TIMEOUT_MS,
|
|
254
253
|
);
|
|
255
254
|
|
|
256
|
-
|
|
257
|
-
"
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
255
|
+
for (const [shape, width, height] of [
|
|
256
|
+
["landscape", 256, 64],
|
|
257
|
+
["portrait", 64, 256],
|
|
258
|
+
] as const) {
|
|
259
|
+
nativeTest(
|
|
260
|
+
`cover-crops a ${shape} upload to the full disc`,
|
|
261
|
+
async () => {
|
|
262
|
+
await writeUpload("png", null, { width, height });
|
|
263
|
+
|
|
264
|
+
const image = await pixels((await renderNotificationAvatarPng())!);
|
|
265
|
+
for (const [x, y] of [
|
|
266
|
+
[128, 8],
|
|
267
|
+
[8, 128],
|
|
268
|
+
[128, 248],
|
|
269
|
+
[248, 128],
|
|
270
|
+
]) {
|
|
271
|
+
expect(image.at(x!, y!)).toEqual({
|
|
272
|
+
r: 0x20,
|
|
273
|
+
g: 0x60,
|
|
274
|
+
b: 0xc0,
|
|
275
|
+
a: 255,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
expect(image.at(30, 30).a).toBe(0);
|
|
279
|
+
},
|
|
280
|
+
NATIVE_RENDER_TEST_TIMEOUT_MS,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
273
283
|
|
|
274
284
|
nativeTest(
|
|
275
285
|
"transcodes a WebP upload, which resvg cannot decode on its own",
|
|
@@ -281,20 +291,18 @@ describe("renderNotificationAvatarPng", () => {
|
|
|
281
291
|
|
|
282
292
|
const image = await pixels(png!);
|
|
283
293
|
expect(image.at(CORNER.x, CORNER.y).a).toBe(0);
|
|
284
|
-
expect(image.at(DISC_SAMPLE.x, DISC_SAMPLE.y)).toEqual({
|
|
285
|
-
...FALLBACK_DISC,
|
|
286
|
-
a: 255,
|
|
287
|
-
});
|
|
288
294
|
// WebP is lossy, so the transcode moves the flat fill by a channel step
|
|
289
295
|
// or two; what matters is that the avatar is drawn at all.
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
[
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
296
|
+
for (const point of [{ x: 128, y: 128 }, DISC_SAMPLE]) {
|
|
297
|
+
const pixel = image.at(point.x, point.y);
|
|
298
|
+
expect(pixel.a).toBe(255);
|
|
299
|
+
for (const [actual, expected] of [
|
|
300
|
+
[pixel.r, 0x20],
|
|
301
|
+
[pixel.g, 0x60],
|
|
302
|
+
[pixel.b, 0xc0],
|
|
303
|
+
]) {
|
|
304
|
+
expect(Math.abs(actual! - expected!)).toBeLessThanOrEqual(4);
|
|
305
|
+
}
|
|
298
306
|
}
|
|
299
307
|
},
|
|
300
308
|
NATIVE_RENDER_TEST_TIMEOUT_MS,
|
|
@@ -222,6 +222,7 @@ export async function renderNotificationAvatarPng(
|
|
|
222
222
|
const Resvg = getResvg();
|
|
223
223
|
const resvg = new Resvg(
|
|
224
224
|
notificationAvatarSvg({
|
|
225
|
+
kind: state.kind,
|
|
225
226
|
innerPngBase64: source.bytes.toString("base64"),
|
|
226
227
|
innerMediaType: source.mediaType,
|
|
227
228
|
accentHex: resolveNotificationAccentHex(state),
|
|
@@ -4355,6 +4355,24 @@ describe("call-controller", () => {
|
|
|
4355
4355
|
controller.destroy();
|
|
4356
4356
|
});
|
|
4357
4357
|
|
|
4358
|
+
test("a terminal verdict hands off without repeating the streamed bridge", async () => {
|
|
4359
|
+
const bridge = "Let me check that.";
|
|
4360
|
+
const { calls } = scriptLegs([
|
|
4361
|
+
[bridge, " [", "ESCALATE", "]"],
|
|
4362
|
+
["The answer is 42."],
|
|
4363
|
+
]);
|
|
4364
|
+
const { relay, controller } = setupController();
|
|
4365
|
+
|
|
4366
|
+
await controller.handleCallerUtterance("What is six times seven?");
|
|
4367
|
+
|
|
4368
|
+
expect(calls).toHaveLength(2);
|
|
4369
|
+
expect(calls[1].routingLeg).toBe("escalated");
|
|
4370
|
+
expect(calls[1].spokenEscalationBridge).toBe(bridge);
|
|
4371
|
+
expect(spokenText(relay)).toBe(`${bridge} The answer is 42.`);
|
|
4372
|
+
expect(relay.sentTokens.filter((t) => t.last)).toHaveLength(1);
|
|
4373
|
+
controller.destroy();
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4358
4376
|
test("a bare escalate verdict speaks the canned bridge, audio-only", async () => {
|
|
4359
4377
|
const { calls } = scriptLegs([["[1]"], ["Forty-two."]]);
|
|
4360
4378
|
const { session, relay, controller } = setupController();
|
|
@@ -17,10 +17,13 @@ import {
|
|
|
17
17
|
describe("front-door verdict tokens", () => {
|
|
18
18
|
test("token constants are the expected bracketed forms", () => {
|
|
19
19
|
expect(HOLD_VERDICT_TOKEN).toBe("[0]");
|
|
20
|
-
expect(ESCALATE_VERDICT_TOKEN).toBe("[
|
|
20
|
+
expect(ESCALATE_VERDICT_TOKEN).toBe("[ESCALATE]");
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
test("stripInternalSpeechMarkers removes both tokens so they are never spoken", () => {
|
|
24
|
+
expect(stripInternalSpeechMarkers("[ESCALATE] Let me check.").trim()).toBe(
|
|
25
|
+
"Let me check.",
|
|
26
|
+
);
|
|
24
27
|
expect(
|
|
25
28
|
stripInternalSpeechMarkers("[1] Let me think about that.").trim(),
|
|
26
29
|
).toBe("Let me think about that.");
|
|
@@ -59,6 +62,7 @@ describe("isIncompleteControlMarkerTail", () => {
|
|
|
59
62
|
"[-",
|
|
60
63
|
"[-1",
|
|
61
64
|
"[END_CAL",
|
|
65
|
+
"[ESCALAT",
|
|
62
66
|
"[TASK:ST",
|
|
63
67
|
"[ASK_GUARDIAN_APPRO",
|
|
64
68
|
]) {
|
|
@@ -70,6 +74,7 @@ describe("isIncompleteControlMarkerTail", () => {
|
|
|
70
74
|
for (const tail of [
|
|
71
75
|
"[-1]",
|
|
72
76
|
"[END_CALL]",
|
|
77
|
+
"[ESCALATE]",
|
|
73
78
|
TASK_STOP_MARKER,
|
|
74
79
|
"[0] answer",
|
|
75
80
|
"[-1] look here",
|
|
@@ -148,6 +148,48 @@ describe("createFrontDoorLegCoordinator", () => {
|
|
|
148
148
|
expect(recorded.escalated.length).toBe(1);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
test("a terminal verdict hands off once with the already released speech", () => {
|
|
152
|
+
const { coordinator, recorded } = harness();
|
|
153
|
+
const bridge = "I will highlight the Rotate control on your screen.";
|
|
154
|
+
coordinator.push(bridge);
|
|
155
|
+
expect(recorded.answers).toEqual([bridge]);
|
|
156
|
+
coordinator.push(" [");
|
|
157
|
+
coordinator.push("ESCALATE]");
|
|
158
|
+
expect(coordinator.handedOff).toBe(false);
|
|
159
|
+
|
|
160
|
+
expect(coordinator.complete()).toBe(true);
|
|
161
|
+
expect(recorded.bridges).toEqual([
|
|
162
|
+
{ spokenBridge: bridge, usesFallback: false, alreadyReleased: true },
|
|
163
|
+
]);
|
|
164
|
+
expect(recorded.escalated).toEqual([escalatedLegFor(bridge)]);
|
|
165
|
+
expect(recorded.events).not.toContain("overrule");
|
|
166
|
+
coordinator.complete();
|
|
167
|
+
coordinator.push("late text");
|
|
168
|
+
expect(recorded.escalated).toHaveLength(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("a cancelled answer with a terminal verdict never hands off", () => {
|
|
172
|
+
let live = true;
|
|
173
|
+
const { coordinator, recorded } = harness({ live: () => live });
|
|
174
|
+
coordinator.push("I will highlight it. [ESCALATE]");
|
|
175
|
+
live = false;
|
|
176
|
+
expect(coordinator.complete()).toBe(false);
|
|
177
|
+
expect(recorded.bridges).toEqual([]);
|
|
178
|
+
expect(recorded.escalated).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test.each(["Select option [1]", "The source is listed as [1]"])(
|
|
182
|
+
"a numbered reference does not start another leg: %s",
|
|
183
|
+
(answer) => {
|
|
184
|
+
const { coordinator, recorded } = harness();
|
|
185
|
+
coordinator.push(answer);
|
|
186
|
+
expect(coordinator.complete()).toBe(false);
|
|
187
|
+
expect(recorded.answers).toEqual([answer]);
|
|
188
|
+
expect(recorded.bridges).toEqual([]);
|
|
189
|
+
expect(recorded.escalated).toEqual([]);
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
151
193
|
test("a leg that stops mid-bridge hands off on completion with the canned bridge", () => {
|
|
152
194
|
const { coordinator, recorded } = harness({ language: "es" });
|
|
153
195
|
coordinator.push("[1]");
|
|
@@ -143,6 +143,10 @@ mock.module("../../persistence/conversation-crud.js", () => ({
|
|
|
143
143
|
recordConversationPersistedSeq: () => {},
|
|
144
144
|
}));
|
|
145
145
|
|
|
146
|
+
import {
|
|
147
|
+
clearHubClients,
|
|
148
|
+
registerHubClient,
|
|
149
|
+
} from "../../__tests__/helpers/hub-clients.js";
|
|
146
150
|
import { setConfig } from "../../__tests__/helpers/set-config.js";
|
|
147
151
|
import type { PreparedModelCall } from "../../agent/loop.js";
|
|
148
152
|
import { selectWinningProfile } from "../../config/llm-resolver.js";
|
|
@@ -2326,21 +2330,30 @@ describe("front-door leg tool suppression", () => {
|
|
|
2326
2330
|
});
|
|
2327
2331
|
|
|
2328
2332
|
describe("desktop skill preactivation", () => {
|
|
2333
|
+
afterEach(() => clearHubClients(assistantEventHub));
|
|
2334
|
+
|
|
2329
2335
|
async function preactivatedFor(turn: Record<string, unknown>): Promise<{
|
|
2330
2336
|
skillIds: string[];
|
|
2331
2337
|
proxyInterfaces: unknown[];
|
|
2332
2338
|
skillIdsDuringLoop: string[];
|
|
2339
|
+
promptDuringLoop: string | null;
|
|
2333
2340
|
}> {
|
|
2334
2341
|
const skillIds: string[] = [];
|
|
2335
2342
|
const proxyInterfaces: unknown[] = [];
|
|
2336
2343
|
let skillIdsDuringLoop: string[] = [];
|
|
2344
|
+
let prompt: string | null = null;
|
|
2345
|
+
let promptDuringLoop: string | null = null;
|
|
2337
2346
|
const fake = makeFakeConversation({
|
|
2338
2347
|
processing: false,
|
|
2339
2348
|
runAgentLoop: async () => {
|
|
2340
2349
|
skillIdsDuringLoop = [...skillIds];
|
|
2350
|
+
promptDuringLoop = prompt;
|
|
2341
2351
|
},
|
|
2342
2352
|
});
|
|
2343
2353
|
Object.assign(fake.conversation, {
|
|
2354
|
+
setVoiceCallControlPrompt: (value: string | null) => {
|
|
2355
|
+
prompt = value;
|
|
2356
|
+
},
|
|
2344
2357
|
addPreactivatedSkillId: (id: string) => {
|
|
2345
2358
|
skillIds.push(id);
|
|
2346
2359
|
},
|
|
@@ -2356,9 +2369,58 @@ describe("desktop skill preactivation", () => {
|
|
|
2356
2369
|
...turn,
|
|
2357
2370
|
});
|
|
2358
2371
|
await flushMicrotasks();
|
|
2359
|
-
return { skillIds, proxyInterfaces, skillIdsDuringLoop };
|
|
2372
|
+
return { skillIds, proxyInterfaces, skillIdsDuringLoop, promptDuringLoop };
|
|
2360
2373
|
}
|
|
2361
2374
|
|
|
2375
|
+
test("a shared-screen turn receives the annotation instructions and schemas before inference", async () => {
|
|
2376
|
+
registerHubClient({
|
|
2377
|
+
hub: assistantEventHub,
|
|
2378
|
+
clientId: "annotation-client",
|
|
2379
|
+
interfaceId: "macos",
|
|
2380
|
+
actorPrincipalId: "user-123",
|
|
2381
|
+
capabilities: ["host_cu", "host_cu_annotate"],
|
|
2382
|
+
});
|
|
2383
|
+
const result = await preactivatedFor({
|
|
2384
|
+
routingLeg: "escalated",
|
|
2385
|
+
macosDesktopSession: true,
|
|
2386
|
+
screenSharing: true,
|
|
2387
|
+
actorPrincipalId: "user-123",
|
|
2388
|
+
});
|
|
2389
|
+
expect(result.skillIdsDuringLoop).toContain("screen-annotation");
|
|
2390
|
+
expect(result.promptDuringLoop).toContain("ID: screen-annotation");
|
|
2391
|
+
expect(result.promptDuringLoop).toContain("Name the thing.");
|
|
2392
|
+
expect(result.promptDuringLoop).toContain("screen_point_at");
|
|
2393
|
+
expect(result.promptDuringLoop).toContain("screen_clear_marks");
|
|
2394
|
+
expect(result.promptDuringLoop).toContain('"target"');
|
|
2395
|
+
expect(result.promptDuringLoop).toContain("skill_execute");
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
test.each([
|
|
2399
|
+
{ screenSharing: false },
|
|
2400
|
+
{ actorPrincipalId: "other-user" },
|
|
2401
|
+
{ routingLeg: "front-door" },
|
|
2402
|
+
{ macosDesktopSession: false },
|
|
2403
|
+
])(
|
|
2404
|
+
"does not preload annotation for an ineligible turn: %j",
|
|
2405
|
+
async (override) => {
|
|
2406
|
+
registerHubClient({
|
|
2407
|
+
hub: assistantEventHub,
|
|
2408
|
+
clientId: "annotation-client",
|
|
2409
|
+
interfaceId: "macos",
|
|
2410
|
+
actorPrincipalId: "user-123",
|
|
2411
|
+
capabilities: ["host_cu", "host_cu_annotate"],
|
|
2412
|
+
});
|
|
2413
|
+
const result = await preactivatedFor({
|
|
2414
|
+
routingLeg: "escalated",
|
|
2415
|
+
macosDesktopSession: true,
|
|
2416
|
+
screenSharing: true,
|
|
2417
|
+
actorPrincipalId: "user-123",
|
|
2418
|
+
...override,
|
|
2419
|
+
});
|
|
2420
|
+
expect(result.promptDuringLoop).not.toContain("ID: screen-annotation");
|
|
2421
|
+
},
|
|
2422
|
+
);
|
|
2423
|
+
|
|
2362
2424
|
test("an escalated leg of a macOS desktop session starts with computer use active", async () => {
|
|
2363
2425
|
const result = await preactivatedFor({
|
|
2364
2426
|
routingLeg: "escalated",
|
|
@@ -2423,6 +2485,18 @@ describe("cutFrontDoorContentAtVerdict", () => {
|
|
|
2423
2485
|
expect(cut?.spokenText).toBe("");
|
|
2424
2486
|
});
|
|
2425
2487
|
|
|
2488
|
+
test("a terminal verdict split across blocks preserves all released speech", () => {
|
|
2489
|
+
const bridge = "Let me check. I will highlight the Rotate control.";
|
|
2490
|
+
const cut = cutFrontDoorContentAtVerdict([
|
|
2491
|
+
{ type: "text", text: `${bridge} [` },
|
|
2492
|
+
{ type: "text", text: "ESCALATE] " },
|
|
2493
|
+
]);
|
|
2494
|
+
expect(cut).toEqual({
|
|
2495
|
+
blocks: [{ type: "text", text: bridge }],
|
|
2496
|
+
spokenText: bridge,
|
|
2497
|
+
});
|
|
2498
|
+
});
|
|
2499
|
+
|
|
2426
2500
|
test("stray verdict tokens inside an answer are stripped, not treated as escalation", () => {
|
|
2427
2501
|
const cut = cutFrontDoorContentAtVerdict([
|
|
2428
2502
|
{ type: "text", text: "It is Tuesday [0] indeed." },
|
|
@@ -2449,7 +2523,7 @@ describe("front-door hub stream gate", () => {
|
|
|
2449
2523
|
* `deltas` in order, then ends the leg with `finalEvent`.
|
|
2450
2524
|
*/
|
|
2451
2525
|
function makeStreamingConversation(
|
|
2452
|
-
deltas: string[],
|
|
2526
|
+
deltas: readonly string[],
|
|
2453
2527
|
finalEvent:
|
|
2454
2528
|
| "message_complete"
|
|
2455
2529
|
| "generation_cancelled" = "message_complete",
|
|
@@ -2533,6 +2607,22 @@ describe("front-door hub stream gate", () => {
|
|
|
2533
2607
|
expect(texts.join("")).toBe("It is Tuesday, and it is sunny.");
|
|
2534
2608
|
});
|
|
2535
2609
|
|
|
2610
|
+
test("a terminal escalation never broadcasts marker fragments", async () => {
|
|
2611
|
+
makeStreamingConversation([
|
|
2612
|
+
"I will highlight it.",
|
|
2613
|
+
" [",
|
|
2614
|
+
"ESC",
|
|
2615
|
+
"ALATE",
|
|
2616
|
+
"]",
|
|
2617
|
+
]);
|
|
2618
|
+
|
|
2619
|
+
const texts = await collectBroadcastText(() =>
|
|
2620
|
+
startVoiceTurn({ ...makeTurnOptions(), routingLeg: "front-door" }),
|
|
2621
|
+
);
|
|
2622
|
+
|
|
2623
|
+
expect(texts).toEqual(["I will highlight it.", " "]);
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2536
2626
|
test("an answer waits on the escalation judge before reaching the hub", async () => {
|
|
2537
2627
|
let openGate!: () => void;
|
|
2538
2628
|
judgeEscalationGate = new Promise<void>((resolve) => {
|
|
@@ -2556,9 +2646,12 @@ describe("front-door hub stream gate", () => {
|
|
|
2556
2646
|
}
|
|
2557
2647
|
});
|
|
2558
2648
|
|
|
2559
|
-
test(
|
|
2649
|
+
test.each([
|
|
2650
|
+
{ deltas: ["Yeah okay, ", "I'll do it."] },
|
|
2651
|
+
{ deltas: ["[ASK_GUARDIAN:"] },
|
|
2652
|
+
])("an overruled answer never reaches the hub: %j", async ({ deltas }) => {
|
|
2560
2653
|
judgeEscalationVerdict = true;
|
|
2561
|
-
makeStreamingConversation(
|
|
2654
|
+
makeStreamingConversation(deltas);
|
|
2562
2655
|
|
|
2563
2656
|
const texts = await collectBroadcastText(async () => {
|
|
2564
2657
|
const handle = await startVoiceTurn({
|
|
@@ -393,13 +393,14 @@ describe("spokenBridgeText", () => {
|
|
|
393
393
|
expect(spokenBridgeText(ESCALATE_VERDICT_TOKEN)).toBe("");
|
|
394
394
|
});
|
|
395
395
|
|
|
396
|
-
test("is empty
|
|
397
|
-
// A stray token later in an answer is not an escalation under the
|
|
398
|
-
// verdict-first protocol.
|
|
396
|
+
test("is empty for an answer with no leading or terminal verdict", () => {
|
|
399
397
|
expect(spokenBridgeText("It is Tuesday.")).toBe("");
|
|
400
|
-
expect(spokenBridgeText(
|
|
401
|
-
|
|
402
|
-
|
|
398
|
+
expect(spokenBridgeText("The token [1] is reserved.")).toBe("");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("a terminal verdict preserves every sentence already released", () => {
|
|
402
|
+
const bridge = "Let me check. I will highlight the control.";
|
|
403
|
+
expect(spokenBridgeText(`${bridge} [ESCALATE]`)).toBe(bridge);
|
|
403
404
|
});
|
|
404
405
|
});
|
|
405
406
|
|
|
@@ -419,7 +420,7 @@ describe("needsFallbackBridge", () => {
|
|
|
419
420
|
|
|
420
421
|
describe("classifyFrontDoorLeading", () => {
|
|
421
422
|
test("pending while the stream could still become a verdict token", () => {
|
|
422
|
-
for (const leading of ["", "[", "[1"]) {
|
|
423
|
+
for (const leading of ["", "[", "[1", "[E", "[ESCALATE"]) {
|
|
423
424
|
expect(classifyFrontDoorLeading(leading, false)).toBe("pending");
|
|
424
425
|
}
|
|
425
426
|
expect(classifyFrontDoorLeading("[0", true)).toBe("pending");
|
|
@@ -435,6 +436,10 @@ describe("classifyFrontDoorLeading", () => {
|
|
|
435
436
|
});
|
|
436
437
|
|
|
437
438
|
test("escalate on the leading escalate token", () => {
|
|
439
|
+
expect(classifyFrontDoorLeading("[ESCALATE]", false)).toBe("escalate");
|
|
440
|
+
expect(classifyFrontDoorLeading("[ESCALATE] Let me check.", true)).toBe(
|
|
441
|
+
"escalate",
|
|
442
|
+
);
|
|
438
443
|
expect(classifyFrontDoorLeading("[1]", false)).toBe("escalate");
|
|
439
444
|
expect(classifyFrontDoorLeading("[1] Let me check.", true)).toBe(
|
|
440
445
|
"escalate",
|
|
@@ -544,6 +549,38 @@ describe("createFrontDoorVerdictMachine", () => {
|
|
|
544
549
|
expect(undecided.finish()).toEqual({ kind: "done" });
|
|
545
550
|
});
|
|
546
551
|
|
|
552
|
+
test("a terminal verdict recovers at completion across every delta boundary", () => {
|
|
553
|
+
const bridge = "I will highlight the Rotate control on your screen.";
|
|
554
|
+
const text = `${bridge} [ESCALATE] \n`;
|
|
555
|
+
for (let split = 1; split < text.length; split++) {
|
|
556
|
+
const machine = createFrontDoorVerdictMachine(false);
|
|
557
|
+
expect(machine.push(text.slice(0, split)).kind).toBe("answer");
|
|
558
|
+
expect(machine.push(text.slice(split)).kind).toBe("answer");
|
|
559
|
+
expect(machine.finish()).toEqual({ kind: "terminal-escalate", bridge });
|
|
560
|
+
expect(machine.finish()).toEqual({ kind: "done" });
|
|
561
|
+
expect(machine.push("late")).toEqual({ kind: "done" });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test.each([
|
|
566
|
+
"Select option [1]",
|
|
567
|
+
"The source is listed as [1]",
|
|
568
|
+
"The source supports this statement. [1]",
|
|
569
|
+
"I will highlight it. [1]",
|
|
570
|
+
"The token [ESCALATE] is reserved.",
|
|
571
|
+
"I will highlight it. [ESCALATE",
|
|
572
|
+
"The token is `[ESCALATE]`",
|
|
573
|
+
"The token [1] is reserved.",
|
|
574
|
+
"I will highlight it. [1",
|
|
575
|
+
"I will highlight it.[1]",
|
|
576
|
+
"The token is `[1]`",
|
|
577
|
+
"I will highlight it. [0]",
|
|
578
|
+
])("does not recover a nonterminal or incomplete verdict: %s", (text) => {
|
|
579
|
+
const machine = createFrontDoorVerdictMachine(false);
|
|
580
|
+
machine.push(text);
|
|
581
|
+
expect(machine.finish()).toEqual({ kind: "done" });
|
|
582
|
+
});
|
|
583
|
+
|
|
547
584
|
test("hold is a step only when the leg was taught the token", () => {
|
|
548
585
|
expect(kinds([HOLD_VERDICT_TOKEN], true)).toEqual(["hold"]);
|
|
549
586
|
expect(kinds([`${HOLD_VERDICT_TOKEN} trailing`], true)).toEqual(["hold"]);
|
|
@@ -627,15 +664,36 @@ describe("createFrontDoorStreamGate", () => {
|
|
|
627
664
|
expect(release(["[1] Let me check"], { finish: false })).toEqual([]);
|
|
628
665
|
});
|
|
629
666
|
|
|
667
|
+
test("recovering a terminal verdict never releases the answer twice", () => {
|
|
668
|
+
const deltas = ["I will highlight it.", " [", "ESCALATE", "]"];
|
|
669
|
+
expect(release(deltas)).toEqual(["I will highlight it.", " "]);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("terminal verdict fragments never escape at any delta boundary", () => {
|
|
673
|
+
const text = "I will highlight it. [ESCALATE]";
|
|
674
|
+
for (let split = 1; split < text.length; split++) {
|
|
675
|
+
expect(release([text.slice(0, split), text.slice(split)]).join("")).toBe(
|
|
676
|
+
"I will highlight it. ",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("an incomplete marker is flushed only on normal completion", () => {
|
|
682
|
+
expect(release(["An opening bracket ["], { finish: false })).toEqual([
|
|
683
|
+
"An opening bracket ",
|
|
684
|
+
]);
|
|
685
|
+
expect(release(["An opening bracket ["]).join("")).toBe(
|
|
686
|
+
"An opening bracket [",
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
|
|
630
690
|
test("a hold verdict releases nothing", () => {
|
|
631
691
|
expect(release([HOLD_VERDICT_TOKEN], { holdEnabled: true })).toEqual([]);
|
|
632
692
|
});
|
|
633
693
|
|
|
634
|
-
test("the hold token is
|
|
635
|
-
// A non-speculative leg's prompt has no hold branch, so its output must
|
|
636
|
-
// not be swallowed by a token it could only have parroted.
|
|
694
|
+
test("the hold token is stripped without discarding a non-speculative answer", () => {
|
|
637
695
|
expect(release([`${HOLD_VERDICT_TOKEN} is the index.`])).toEqual([
|
|
638
|
-
|
|
696
|
+
" is the index.",
|
|
639
697
|
]);
|
|
640
698
|
});
|
|
641
699
|
|
|
@@ -1158,7 +1158,9 @@ export class CallController {
|
|
|
1158
1158
|
speakFixedPhrase(bridge.spokenBridge, bridge.language);
|
|
1159
1159
|
return;
|
|
1160
1160
|
}
|
|
1161
|
-
|
|
1161
|
+
if (!bridge.alreadyReleased) {
|
|
1162
|
+
fullResponseText += `${bridge.spokenBridge} `;
|
|
1163
|
+
}
|
|
1162
1164
|
flushSafeText(fullResponseText, { force: true });
|
|
1163
1165
|
// Force-synthesize the bridge now. On the synthesized-TTS path text is
|
|
1164
1166
|
// held in pendingSynthText until a sentence boundary, so an
|
|
@@ -28,7 +28,12 @@ export const TASK_UPDATE_SILENT_MARKER = "[TASK_UPDATE:SILENT]";
|
|
|
28
28
|
* markers they are swallowed before reaching TTS — never spoken aloud.
|
|
29
29
|
*/
|
|
30
30
|
export const HOLD_VERDICT_TOKEN = "[0]";
|
|
31
|
-
export const ESCALATE_VERDICT_TOKEN = "[
|
|
31
|
+
export const ESCALATE_VERDICT_TOKEN = "[ESCALATE]";
|
|
32
|
+
const LEGACY_ESCALATE_VERDICT_TOKEN = "[1]";
|
|
33
|
+
export const ESCALATE_VERDICT_TOKENS = [
|
|
34
|
+
ESCALATE_VERDICT_TOKEN,
|
|
35
|
+
LEGACY_ESCALATE_VERDICT_TOKEN,
|
|
36
|
+
] as const;
|
|
32
37
|
|
|
33
38
|
/**
|
|
34
39
|
* Room-minimize token, kept in the marker table so it is stripped from
|
|
@@ -93,7 +98,6 @@ const CALL_OPENING_ACK_MARKER_REGEX = /\[CALL_OPENING_ACK\]/g;
|
|
|
93
98
|
const END_CALL_MARKER_REGEX = /\[END_CALL\]/g;
|
|
94
99
|
const TASK_STOP_MARKER_REGEX = /\[TASK:STOP\]/g;
|
|
95
100
|
const HOLD_VERDICT_TOKEN_REGEX = /\[0\]/g;
|
|
96
|
-
const ESCALATE_VERDICT_TOKEN_REGEX = /\[1\]/g;
|
|
97
101
|
const MINIMIZE_ROOM_MARKER_REGEX = /\[-1\]/g;
|
|
98
102
|
const MUTE_MARKER_REGEX = /\[MUTE(?::\s*[^\]]*)?\]/g;
|
|
99
103
|
const UPDATES_MARKER_REGEX = /\[UPDATES:\s*[^\]]*\]/g;
|
|
@@ -223,7 +227,8 @@ export function stripInternalSpeechMarkers(text: string): string {
|
|
|
223
227
|
.replace(TASK_STOP_MARKER_REGEX, "")
|
|
224
228
|
.replaceAll(TASK_UPDATE_SILENT_MARKER, "")
|
|
225
229
|
.replace(HOLD_VERDICT_TOKEN_REGEX, "")
|
|
226
|
-
.
|
|
230
|
+
.replaceAll(ESCALATE_VERDICT_TOKEN, "")
|
|
231
|
+
.replaceAll(LEGACY_ESCALATE_VERDICT_TOKEN, "")
|
|
227
232
|
.replace(MINIMIZE_ROOM_MARKER_REGEX, "")
|
|
228
233
|
.replace(MUTE_MARKER_REGEX, "")
|
|
229
234
|
.replace(UPDATES_MARKER_REGEX, "")
|
|
@@ -253,7 +258,7 @@ const CONTROL_MARKER_STRINGS = [
|
|
|
253
258
|
TASK_STOP_MARKER,
|
|
254
259
|
TASK_UPDATE_SILENT_MARKER,
|
|
255
260
|
"[0]",
|
|
256
|
-
|
|
261
|
+
...ESCALATE_VERDICT_TOKENS,
|
|
257
262
|
"[-1]",
|
|
258
263
|
MUTE_MARKER,
|
|
259
264
|
MUTE_MARKER_PREFIX,
|