@vellumai/assistant 0.11.1 → 0.11.2-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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the voice activity line.
|
|
3
|
+
*
|
|
4
|
+
* The invariant worth guarding is totality: the tool vocabulary is open
|
|
5
|
+
* (plugins, MCP servers, and skills all contribute names this module has never
|
|
6
|
+
* seen), and the surface this feeds is a Lock Screen the user cannot correct.
|
|
7
|
+
* A label that came back empty would blank the island exactly when the
|
|
8
|
+
* assistant is busiest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
activityLabelForTool,
|
|
15
|
+
approvalActivityLabel,
|
|
16
|
+
GENERIC_ACTIVITY_LABEL,
|
|
17
|
+
} from "../activity-label.js";
|
|
18
|
+
|
|
19
|
+
describe("activityLabelForTool", () => {
|
|
20
|
+
test("names what the user would say is happening, not the tool", () => {
|
|
21
|
+
expect(activityLabelForTool("bash")).toBe("Running a command");
|
|
22
|
+
expect(activityLabelForTool("web_search")).toBe("Searching the web");
|
|
23
|
+
expect(activityLabelForTool("file_read")).toBe("Reading a file");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Where a tool runs is not a distinction anyone glancing at a Lock Screen is
|
|
27
|
+
// making.
|
|
28
|
+
test("host and sandbox variants read the same", () => {
|
|
29
|
+
expect(activityLabelForTool("host_bash")).toBe(
|
|
30
|
+
activityLabelForTool("bash"),
|
|
31
|
+
);
|
|
32
|
+
expect(activityLabelForTool("host_file_read")).toBe(
|
|
33
|
+
activityLabelForTool("file_read"),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("falls back by prefix for structured tool families", () => {
|
|
38
|
+
expect(activityLabelForTool("browser_click")).toBe("Using the browser");
|
|
39
|
+
expect(activityLabelForTool("mcp_notion_search")).toBe(
|
|
40
|
+
"Using a connected app",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("every tool gets a label, including ones this module has never seen", () => {
|
|
45
|
+
for (const name of [
|
|
46
|
+
"slack_conversations_history",
|
|
47
|
+
"",
|
|
48
|
+
"a",
|
|
49
|
+
"some_vendor::weird.tool/name",
|
|
50
|
+
]) {
|
|
51
|
+
expect(activityLabelForTool(name)).toBe(GENERIC_ACTIVITY_LABEL);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("labels stay short enough for the slot they land in", () => {
|
|
56
|
+
for (const name of [
|
|
57
|
+
"bash",
|
|
58
|
+
"web_search",
|
|
59
|
+
"host_file_transfer",
|
|
60
|
+
"computer_use",
|
|
61
|
+
"ui_show",
|
|
62
|
+
"unknown_tool",
|
|
63
|
+
]) {
|
|
64
|
+
const label = activityLabelForTool(name);
|
|
65
|
+
expect(label.length).toBeLessThanOrEqual(32);
|
|
66
|
+
expect(label).not.toMatch(/[.!?]$/);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("approvalActivityLabel", () => {
|
|
72
|
+
// The turn is not running the tool, it is waiting to be allowed to. Saying
|
|
73
|
+
// the former is the misstatement this exists to stop — and the line has to
|
|
74
|
+
// name *something*, or the island asks the user to approve a blank.
|
|
75
|
+
test("keeps the tool's phrase and says who is being waited on", () => {
|
|
76
|
+
expect(approvalActivityLabel("bash")).toBe(
|
|
77
|
+
"Running a command — needs your okay",
|
|
78
|
+
);
|
|
79
|
+
expect(approvalActivityLabel("web_search")).toBe(
|
|
80
|
+
"Searching the web — needs your okay",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("an unrecognized tool still reads as a wait", () => {
|
|
85
|
+
expect(approvalActivityLabel("some_vendor_tool")).toBe(
|
|
86
|
+
`${GENERIC_ACTIVITY_LABEL} — needs your okay`,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// A confirmation from a proxy or network prompter has no tool behind it, and
|
|
91
|
+
// "Working on it — needs your okay" would claim work that is not happening.
|
|
92
|
+
test("drops the phrase entirely when there is no tool to name", () => {
|
|
93
|
+
expect(approvalActivityLabel("")).toBe("Needs your okay");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -34,16 +34,26 @@ function sttFinal(text: string): LiveVoiceServerFramePayload {
|
|
|
34
34
|
|
|
35
35
|
/** A reporter whose dispatches are captured instead of sent. */
|
|
36
36
|
class RecordingReporter extends LiveActivityReporter {
|
|
37
|
-
readonly dispatched: Array<{ phase: string; event: string }> =
|
|
37
|
+
readonly dispatched: Array<{ phase: string; event: string; detail: string }> =
|
|
38
|
+
[];
|
|
38
39
|
|
|
39
40
|
protected override async dispatch(
|
|
40
41
|
phase: string,
|
|
41
42
|
event: "update" | "end",
|
|
43
|
+
detail: string,
|
|
42
44
|
): Promise<void> {
|
|
43
|
-
this.dispatched.push({ phase, event });
|
|
45
|
+
this.dispatched.push({ phase, event, detail });
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
function activity(label: string): LiveVoiceServerFramePayload {
|
|
50
|
+
return {
|
|
51
|
+
type: "activity",
|
|
52
|
+
turnId: "t1",
|
|
53
|
+
label,
|
|
54
|
+
} as LiveVoiceServerFramePayload;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
describe("phaseForFrame", () => {
|
|
48
58
|
test("maps the frames the client derives phases from", () => {
|
|
49
59
|
expect(phaseForFrame(frame("utterance_end"), "listening")).toBe(
|
|
@@ -92,7 +102,7 @@ describe("LiveActivityReporter", () => {
|
|
|
92
102
|
reporter.report(frame("thinking"));
|
|
93
103
|
|
|
94
104
|
expect(reporter.dispatched).toEqual([
|
|
95
|
-
{ phase: "thinking", event: "update" },
|
|
105
|
+
{ phase: "thinking", event: "update", detail: "" },
|
|
96
106
|
]);
|
|
97
107
|
});
|
|
98
108
|
|
|
@@ -105,7 +115,7 @@ describe("LiveActivityReporter", () => {
|
|
|
105
115
|
reporter.report(frame("tts_audio"));
|
|
106
116
|
|
|
107
117
|
expect(reporter.dispatched).toEqual([
|
|
108
|
-
{ phase: "speaking", event: "update" },
|
|
118
|
+
{ phase: "speaking", event: "update", detail: "" },
|
|
109
119
|
]);
|
|
110
120
|
});
|
|
111
121
|
|
|
@@ -144,6 +154,7 @@ describe("LiveActivityReporter", () => {
|
|
|
144
154
|
expect(reporter.dispatched.at(-1)).toEqual({
|
|
145
155
|
phase: "ending",
|
|
146
156
|
event: "end",
|
|
157
|
+
detail: "",
|
|
147
158
|
});
|
|
148
159
|
});
|
|
149
160
|
|
|
@@ -165,7 +176,77 @@ describe("LiveActivityReporter", () => {
|
|
|
165
176
|
reporter.end();
|
|
166
177
|
reporter.report(frame("tts_audio"));
|
|
167
178
|
|
|
168
|
-
expect(reporter.dispatched).toEqual([
|
|
179
|
+
expect(reporter.dispatched).toEqual([
|
|
180
|
+
{ phase: "ending", event: "end", detail: "" },
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("LiveActivityReporter activity line", () => {
|
|
186
|
+
test("carries the activity label alongside the phase it holds", () => {
|
|
187
|
+
const reporter = new RecordingReporter("conv-1");
|
|
188
|
+
|
|
189
|
+
reporter.report(frame("utterance_end"));
|
|
190
|
+
reporter.report(frame("thinking"));
|
|
191
|
+
reporter.report(activity("Running a command"));
|
|
192
|
+
|
|
193
|
+
expect(reporter.dispatched.at(-1)).toEqual({
|
|
194
|
+
phase: "thinking",
|
|
195
|
+
event: "update",
|
|
196
|
+
detail: "Running a command",
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// A push replaces the whole content state, so the label has to ride along on
|
|
201
|
+
// phase changes too, or the next phase would blank it.
|
|
202
|
+
test("keeps the label through a later phase change", () => {
|
|
203
|
+
const reporter = new RecordingReporter("conv-1");
|
|
204
|
+
|
|
205
|
+
reporter.report(frame("thinking"));
|
|
206
|
+
reporter.report(activity("Reading a file"));
|
|
207
|
+
reporter.report(frame("tts_audio"));
|
|
208
|
+
|
|
209
|
+
expect(reporter.dispatched.at(-1)).toEqual({
|
|
210
|
+
phase: "speaking",
|
|
211
|
+
event: "update",
|
|
212
|
+
detail: "Reading a file",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("an empty label clears the line", () => {
|
|
217
|
+
const reporter = new RecordingReporter("conv-1");
|
|
218
|
+
|
|
219
|
+
reporter.report(frame("thinking"));
|
|
220
|
+
reporter.report(activity("Reading a file"));
|
|
221
|
+
reporter.report(activity(""));
|
|
222
|
+
|
|
223
|
+
expect(reporter.dispatched.at(-1)?.detail).toBe("");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("does not dispatch a label it already sent", () => {
|
|
227
|
+
const reporter = new RecordingReporter("conv-1");
|
|
228
|
+
|
|
229
|
+
reporter.report(frame("thinking"));
|
|
230
|
+
reporter.report(activity("Reading a file"));
|
|
231
|
+
const before = reporter.dispatched.length;
|
|
232
|
+
reporter.report(activity("Reading a file"));
|
|
233
|
+
|
|
234
|
+
expect(reporter.dispatched.length).toBe(before);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// A tool can start before any phase-bearing frame lands. There is no content
|
|
238
|
+
// state to attach a label to yet, and the phase that follows carries it.
|
|
239
|
+
test("holds a label that arrives before any phase", () => {
|
|
240
|
+
const reporter = new RecordingReporter("conv-1");
|
|
241
|
+
|
|
242
|
+
reporter.report(activity("Searching the web"));
|
|
243
|
+
expect(reporter.dispatched).toEqual([]);
|
|
244
|
+
|
|
245
|
+
reporter.report(frame("thinking"));
|
|
246
|
+
|
|
247
|
+
expect(reporter.dispatched).toEqual([
|
|
248
|
+
{ phase: "thinking", event: "update", detail: "Searching the web" },
|
|
249
|
+
]);
|
|
169
250
|
});
|
|
170
251
|
});
|
|
171
252
|
|
|
@@ -154,16 +154,42 @@ async function waitFor(
|
|
|
154
154
|
throw new Error(message);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The most recent `activity` frame, or `undefined` when none has been sent.
|
|
159
|
+
* `findLast` is past this project's lib target, so the filter-and-take-last
|
|
160
|
+
* form stands in for it.
|
|
161
|
+
*/
|
|
162
|
+
function lastActivityFrame(
|
|
163
|
+
frames: LiveVoiceServerFrame[],
|
|
164
|
+
): Extract<LiveVoiceServerFrame, { type: "activity" }> | undefined {
|
|
165
|
+
const activity = frames.filter(
|
|
166
|
+
(frame): frame is Extract<LiveVoiceServerFrame, { type: "activity" }> =>
|
|
167
|
+
frame.type === "activity",
|
|
168
|
+
);
|
|
169
|
+
return activity[activity.length - 1];
|
|
170
|
+
}
|
|
171
|
+
|
|
157
172
|
function createCapturingTurnStarter(): {
|
|
158
173
|
startVoiceTurn: LiveVoiceTurnStarter;
|
|
159
174
|
getCallbacks: () => VoiceTurnCallbacks | undefined;
|
|
175
|
+
announceApprovalPending: () => void;
|
|
176
|
+
announceApprovalsResolved: () => void;
|
|
160
177
|
} {
|
|
161
178
|
let callbacks: VoiceTurnCallbacks | undefined;
|
|
179
|
+
let onApprovalPending: ((requestId: string) => void) | undefined;
|
|
180
|
+
let onApprovalsResolved: (() => void) | undefined;
|
|
162
181
|
const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
|
|
163
182
|
callbacks = options.callbacks;
|
|
183
|
+
onApprovalPending = options.onApprovalPending;
|
|
184
|
+
onApprovalsResolved = options.onApprovalsResolved;
|
|
164
185
|
return { turnId: "bridge-turn-1", abort: mock() };
|
|
165
186
|
});
|
|
166
|
-
return {
|
|
187
|
+
return {
|
|
188
|
+
startVoiceTurn,
|
|
189
|
+
getCallbacks: () => callbacks,
|
|
190
|
+
announceApprovalPending: () => onApprovalPending?.("req-1"),
|
|
191
|
+
announceApprovalsResolved: () => onApprovalsResolved?.(),
|
|
192
|
+
};
|
|
167
193
|
}
|
|
168
194
|
|
|
169
195
|
function createRecordingTtsStreamer(): {
|
|
@@ -442,7 +468,7 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
442
468
|
});
|
|
443
469
|
});
|
|
444
470
|
|
|
445
|
-
test("records tool_use_start on the active turn
|
|
471
|
+
test("records tool_use_start on the active turn and publishes what it is doing", async () => {
|
|
446
472
|
let callbacks: VoiceTurnCallbacks | undefined;
|
|
447
473
|
const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
|
|
448
474
|
callbacks = options.callbacks;
|
|
@@ -459,9 +485,8 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
459
485
|
"thinking",
|
|
460
486
|
]);
|
|
461
487
|
|
|
462
|
-
const frameCountBefore = frames.length;
|
|
463
488
|
callbacks?.tool_use_start?.("some_tool");
|
|
464
|
-
await
|
|
489
|
+
await waitForFrameCount(frames, 4);
|
|
465
490
|
|
|
466
491
|
const activeTurn = (
|
|
467
492
|
session as unknown as {
|
|
@@ -469,20 +494,30 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
469
494
|
}
|
|
470
495
|
).activeAssistantTurn;
|
|
471
496
|
expect(activeTurn?.toolUseStarted).toBe(true);
|
|
472
|
-
|
|
497
|
+
// One activity frame, so a silent stretch of tool work is visible on the
|
|
498
|
+
// surfaces that show the session. An unrecognized tool still gets a line.
|
|
499
|
+
expect(frames.at(-1)).toMatchObject({
|
|
500
|
+
type: "activity",
|
|
501
|
+
label: "Working on it",
|
|
502
|
+
});
|
|
473
503
|
|
|
474
504
|
callbacks?.message_complete?.({
|
|
475
505
|
type: "message_complete",
|
|
476
506
|
conversationId: "conversation-123",
|
|
477
507
|
messageId: "assistant-message-123",
|
|
478
508
|
});
|
|
479
|
-
await waitForFrameCount(frames,
|
|
509
|
+
await waitForFrameCount(frames, 6);
|
|
480
510
|
expect(frames.map((frame) => frame.type)).toEqual([
|
|
481
511
|
"ready",
|
|
482
512
|
"stt_final",
|
|
483
513
|
"thinking",
|
|
514
|
+
"activity",
|
|
484
515
|
"tts_done",
|
|
516
|
+
// The turn is over, so the line clears: no surface should keep showing
|
|
517
|
+
// the last tool it touched through the silence that follows.
|
|
518
|
+
"activity",
|
|
485
519
|
]);
|
|
520
|
+
expect(frames.at(-1)).toMatchObject({ type: "activity", label: "" });
|
|
486
521
|
});
|
|
487
522
|
|
|
488
523
|
test("interrupt aborts the in-flight turn and ignores late bridge events", async () => {
|
|
@@ -609,7 +644,7 @@ describe("LiveVoiceSession tool-use spoken ack", () => {
|
|
|
609
644
|
});
|
|
610
645
|
});
|
|
611
646
|
|
|
612
|
-
describe("LiveVoiceSession
|
|
647
|
+
describe("LiveVoiceSession room reveal", () => {
|
|
613
648
|
function createMarkerHarness() {
|
|
614
649
|
const { startVoiceTurn, getCallbacks } = createCapturingTurnStarter();
|
|
615
650
|
const { streamTtsAudio, ttsTexts } = createRecordingTtsStreamer();
|
|
@@ -624,11 +659,10 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
624
659
|
}
|
|
625
660
|
|
|
626
661
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* latch `minimizeRequested`.
|
|
662
|
+
* Harness for the leg that can actually put something on screen: routing
|
|
663
|
+
* fronts every turn with the fast leg, so the front-door call is scripted to
|
|
664
|
+
* escalate ("[1] Working on it.") and the returned callbacks drive the
|
|
665
|
+
* ESCALATED leg; the toolless front door can never run a ui tool.
|
|
632
666
|
*/
|
|
633
667
|
function createEscalatedMarkerHarness() {
|
|
634
668
|
let escalatedCallbacks: VoiceTurnCallbacks | undefined;
|
|
@@ -664,35 +698,285 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
664
698
|
};
|
|
665
699
|
}
|
|
666
700
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
701
|
+
/** Run a ui tool to completion, as the agent loop would. */
|
|
702
|
+
function runUiTool(
|
|
703
|
+
getCallbacks: () => VoiceTurnCallbacks | undefined,
|
|
704
|
+
toolName: string,
|
|
705
|
+
opts?: { isError?: boolean },
|
|
706
|
+
): void {
|
|
707
|
+
getCallbacks()?.tool_use_start?.(toolName);
|
|
708
|
+
getCallbacks()?.tool_result?.({
|
|
709
|
+
toolName,
|
|
710
|
+
resultPreview: "",
|
|
711
|
+
...(opts?.isError === true ? { isError: true } : {}),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Showing a surface is what reveals the screen. Nothing the model says does,
|
|
716
|
+
// so the user never loses the reveal to a forgotten token.
|
|
717
|
+
test("a ui tool emits minimize_room after tts_done", async () => {
|
|
718
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
670
719
|
|
|
671
720
|
await startReleasedTurn(session, getCallbacks);
|
|
672
|
-
|
|
721
|
+
runUiTool(getCallbacks, "ui_show");
|
|
722
|
+
emitTextDelta(getCallbacks, "Here is the summary.");
|
|
673
723
|
emitMessageComplete(getCallbacks);
|
|
674
724
|
await waitFor(() => frames.some((frame) => frame.type === "minimize_room"));
|
|
675
725
|
|
|
676
|
-
const joined = assistantDeltaTexts(frames).join("");
|
|
677
|
-
expect(joined).toContain("hello world. ");
|
|
678
|
-
expect(joined).not.toContain("[-1]");
|
|
679
|
-
expect(ttsTexts.join(" ")).not.toContain("[-1]");
|
|
680
726
|
const ttsDoneIndex = frames.findIndex((frame) => frame.type === "tts_done");
|
|
681
727
|
const minimizeIndex = frames.findIndex(
|
|
682
728
|
(frame) => frame.type === "minimize_room",
|
|
683
729
|
);
|
|
684
730
|
expect(ttsDoneIndex).toBeGreaterThanOrEqual(0);
|
|
731
|
+
// After the speech drains, never mid-sentence.
|
|
685
732
|
expect(minimizeIndex).toBeGreaterThan(ttsDoneIndex);
|
|
686
733
|
expect(frames[minimizeIndex]).toMatchObject({
|
|
687
734
|
type: "minimize_room",
|
|
688
735
|
turnId: "live-turn-1",
|
|
689
736
|
});
|
|
737
|
+
// At most once, however many surfaces the turn touched.
|
|
690
738
|
expect(
|
|
691
739
|
frames.filter((frame) => frame.type === "minimize_room"),
|
|
692
740
|
).toHaveLength(1);
|
|
693
741
|
});
|
|
694
742
|
|
|
695
|
-
|
|
743
|
+
// The reveal follows the surface, not the attempt: a rejected call (no
|
|
744
|
+
// surface_type, an empty card, a client that never acks) rendered nothing,
|
|
745
|
+
// and minimizing to show nothing is worse than staying put.
|
|
746
|
+
test("a failed ui call reveals nothing", async () => {
|
|
747
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
748
|
+
|
|
749
|
+
await startReleasedTurn(session, getCallbacks);
|
|
750
|
+
runUiTool(getCallbacks, "ui_show", { isError: true });
|
|
751
|
+
emitTextDelta(getCallbacks, "I could not show that.");
|
|
752
|
+
emitMessageComplete(getCallbacks);
|
|
753
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
754
|
+
|
|
755
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("a surface taken away before the reply ends reveals nothing", async () => {
|
|
759
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
760
|
+
|
|
761
|
+
await startReleasedTurn(session, getCallbacks);
|
|
762
|
+
runUiTool(getCallbacks, "ui_show");
|
|
763
|
+
runUiTool(getCallbacks, "ui_dismiss");
|
|
764
|
+
emitTextDelta(getCallbacks, "Never mind, sorted it.");
|
|
765
|
+
emitMessageComplete(getCallbacks);
|
|
766
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
767
|
+
|
|
768
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
// Opening an app puts something on screen just as a card does; a list keyed
|
|
772
|
+
// on "ui_" would have missed it.
|
|
773
|
+
test("opening an app reveals the screen", async () => {
|
|
774
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
775
|
+
|
|
776
|
+
await startReleasedTurn(session, getCallbacks);
|
|
777
|
+
runUiTool(getCallbacks, "app_open");
|
|
778
|
+
emitTextDelta(getCallbacks, "Opened it up.");
|
|
779
|
+
emitMessageComplete(getCallbacks);
|
|
780
|
+
await waitFor(() => frames.some((frame) => frame.type === "minimize_room"));
|
|
781
|
+
|
|
782
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(true);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// A blocked turn has no speech left to drain, and the card it is blocked on
|
|
786
|
+
// renders behind the room. Waiting for a drain that is not coming would
|
|
787
|
+
// leave the call silent in front of a decision the user cannot see.
|
|
788
|
+
test("a pending approval reveals the screen immediately", async () => {
|
|
789
|
+
const { startVoiceTurn, announceApprovalPending } =
|
|
790
|
+
createCapturingTurnStarter();
|
|
791
|
+
const { frames, session } = createSessionHarness({ startVoiceTurn });
|
|
792
|
+
|
|
793
|
+
await session.start();
|
|
794
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
795
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
796
|
+
|
|
797
|
+
announceApprovalPending();
|
|
798
|
+
await waitFor(() => frames.some((frame) => frame.type === "minimize_room"));
|
|
799
|
+
|
|
800
|
+
// Before any tts_done, unlike the drain-scoped reveal a shown surface gets.
|
|
801
|
+
expect(frames.some((frame) => frame.type === "tts_done")).toBe(false);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Every phrase progress narration has describes work in flight ("still on
|
|
805
|
+
// it", "almost there"). While the turn is blocked on the user, all of them
|
|
806
|
+
// are false, and the one that matters is the one about who is being waited
|
|
807
|
+
// on.
|
|
808
|
+
test("says it is waiting, once, and stops narrating progress", async () => {
|
|
809
|
+
const {
|
|
810
|
+
startVoiceTurn,
|
|
811
|
+
announceApprovalPending,
|
|
812
|
+
announceApprovalsResolved,
|
|
813
|
+
} = createCapturingTurnStarter();
|
|
814
|
+
const { streamTtsAudio, ttsTexts } = createRecordingTtsStreamer();
|
|
815
|
+
const { frames, session } = createSessionHarness({
|
|
816
|
+
startVoiceTurn,
|
|
817
|
+
streamTtsAudio,
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
await session.start();
|
|
821
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
822
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
823
|
+
|
|
824
|
+
announceApprovalPending();
|
|
825
|
+
await waitFor(() => ttsTexts.join(" ").includes("okay"));
|
|
826
|
+
// Repeat announcements do not repeat the line.
|
|
827
|
+
announceApprovalPending();
|
|
828
|
+
await flushAsyncCallbacks();
|
|
829
|
+
|
|
830
|
+
const saidWaiting = ttsTexts.filter((text) => text.includes("okay")).length;
|
|
831
|
+
expect(saidWaiting).toBe(1);
|
|
832
|
+
|
|
833
|
+
announceApprovalsResolved();
|
|
834
|
+
await flushAsyncCallbacks();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// `tool_use_start` fires before the approval gate blocks, so without this the
|
|
838
|
+
// island keeps saying "Running a command" for the whole wait — a claim about
|
|
839
|
+
// work in flight made at the one moment nothing is in flight. The request id
|
|
840
|
+
// is what turns that line from accurate into answerable: it is what the Live
|
|
841
|
+
// Activity's Approve and Deny send back.
|
|
842
|
+
test("a pending approval publishes an answerable activity line", async () => {
|
|
843
|
+
const { startVoiceTurn, getCallbacks, announceApprovalPending } =
|
|
844
|
+
createCapturingTurnStarter();
|
|
845
|
+
const { frames, session } = createSessionHarness({ startVoiceTurn });
|
|
846
|
+
|
|
847
|
+
await session.start();
|
|
848
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
849
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
850
|
+
|
|
851
|
+
getCallbacks()?.tool_use_start?.("bash");
|
|
852
|
+
await waitFor(() =>
|
|
853
|
+
frames.some(
|
|
854
|
+
(frame) => frame.type === "activity" && frame.label.length > 0,
|
|
855
|
+
),
|
|
856
|
+
);
|
|
857
|
+
announceApprovalPending();
|
|
858
|
+
await waitFor(() =>
|
|
859
|
+
frames.some(
|
|
860
|
+
(frame) =>
|
|
861
|
+
frame.type === "activity" && frame.approvalRequestId === "req-1",
|
|
862
|
+
),
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
const waiting = lastActivityFrame(frames);
|
|
866
|
+
// The tool's own phrase, kept, plus who is being waited on — so the line
|
|
867
|
+
// beside the buttons names what is being approved without naming the tool
|
|
868
|
+
// or its arguments to a Lock Screen.
|
|
869
|
+
expect(waiting).toMatchObject({
|
|
870
|
+
label: "Running a command — needs your okay",
|
|
871
|
+
approvalRequestId: "req-1",
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// However the decision is made — the card in the app, the daemon's own
|
|
876
|
+
// timeout, a superseding message — the buttons must go with it. They are
|
|
877
|
+
// rendered from the id, so retiring the id is what retires them.
|
|
878
|
+
test("resolving the approval retires the request id", async () => {
|
|
879
|
+
const {
|
|
880
|
+
startVoiceTurn,
|
|
881
|
+
getCallbacks,
|
|
882
|
+
announceApprovalPending,
|
|
883
|
+
announceApprovalsResolved,
|
|
884
|
+
} = createCapturingTurnStarter();
|
|
885
|
+
const { frames, session } = createSessionHarness({ startVoiceTurn });
|
|
886
|
+
|
|
887
|
+
await session.start();
|
|
888
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
889
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
890
|
+
|
|
891
|
+
getCallbacks()?.tool_use_start?.("bash");
|
|
892
|
+
announceApprovalPending();
|
|
893
|
+
await waitFor(() =>
|
|
894
|
+
frames.some(
|
|
895
|
+
(frame) =>
|
|
896
|
+
frame.type === "activity" && frame.approvalRequestId === "req-1",
|
|
897
|
+
),
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
announceApprovalsResolved();
|
|
901
|
+
await waitFor(
|
|
902
|
+
() => lastActivityFrame(frames)?.approvalRequestId === undefined,
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
const resumed = lastActivityFrame(frames);
|
|
906
|
+
// Back to the tool that is now genuinely running, with nothing to answer.
|
|
907
|
+
expect(resumed).toMatchObject({ label: "Running a command" });
|
|
908
|
+
expect(resumed?.approvalRequestId).toBeUndefined();
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// A turn can start and finish other work while it is blocked. Each of those
|
|
912
|
+
// events republishes the activity line, and a republish composed as if
|
|
913
|
+
// nothing were pending would take the request id down with it — retiring the
|
|
914
|
+
// island's buttons while the turn was still waiting on them.
|
|
915
|
+
test("a parallel tool event mid-wait does not retire the buttons", async () => {
|
|
916
|
+
const { startVoiceTurn, getCallbacks, announceApprovalPending } =
|
|
917
|
+
createCapturingTurnStarter();
|
|
918
|
+
const { frames, session } = createSessionHarness({ startVoiceTurn });
|
|
919
|
+
|
|
920
|
+
await session.start();
|
|
921
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
922
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
923
|
+
|
|
924
|
+
getCallbacks()?.tool_use_start?.("bash");
|
|
925
|
+
announceApprovalPending();
|
|
926
|
+
await waitFor(() =>
|
|
927
|
+
frames.some(
|
|
928
|
+
(frame) =>
|
|
929
|
+
frame.type === "activity" && frame.approvalRequestId === "req-1",
|
|
930
|
+
),
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
getCallbacks()?.tool_use_start?.("web_search");
|
|
934
|
+
getCallbacks()?.tool_result?.({
|
|
935
|
+
toolName: "web_search",
|
|
936
|
+
resultPreview: "",
|
|
937
|
+
});
|
|
938
|
+
await flushAsyncCallbacks();
|
|
939
|
+
|
|
940
|
+
const latest = lastActivityFrame(frames);
|
|
941
|
+
expect(latest?.approvalRequestId).toBe("req-1");
|
|
942
|
+
// And still naming the tool the decision is about, not the one that ran
|
|
943
|
+
// alongside it.
|
|
944
|
+
expect(latest?.label).toBe("Running a command — needs your okay");
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test("dismissing a surface does not reveal the screen", async () => {
|
|
948
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
949
|
+
|
|
950
|
+
await startReleasedTurn(session, getCallbacks);
|
|
951
|
+
runUiTool(getCallbacks, "ui_dismiss");
|
|
952
|
+
emitTextDelta(getCallbacks, "Cleared that away.");
|
|
953
|
+
emitMessageComplete(getCallbacks);
|
|
954
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
955
|
+
|
|
956
|
+
// Minimizing to show the user something that is no longer there is the
|
|
957
|
+
// opposite of the point.
|
|
958
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// The marker is no longer taught to any leg, so this is about a model that
|
|
962
|
+
// emits one anyway: it must not be spoken, and it must not move the room.
|
|
963
|
+
test("a terminal [-1] is stripped from deltas and TTS and reveals nothing", async () => {
|
|
964
|
+
const { frames, session, getCallbacks, ttsTexts } =
|
|
965
|
+
createEscalatedMarkerHarness();
|
|
966
|
+
|
|
967
|
+
await startReleasedTurn(session, getCallbacks);
|
|
968
|
+
emitTextDelta(getCallbacks, "hello world. [-1]");
|
|
969
|
+
emitMessageComplete(getCallbacks);
|
|
970
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
971
|
+
|
|
972
|
+
const joined = assistantDeltaTexts(frames).join("");
|
|
973
|
+
expect(joined).toContain("hello world. ");
|
|
974
|
+
expect(joined).not.toContain("[-1]");
|
|
975
|
+
expect(ttsTexts.join(" ")).not.toContain("[-1]");
|
|
976
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("holds a marker split across deltas and never leaks the partial", async () => {
|
|
696
980
|
const { frames, session, getCallbacks, ttsTexts } =
|
|
697
981
|
createEscalatedMarkerHarness();
|
|
698
982
|
|
|
@@ -703,14 +987,14 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
703
987
|
|
|
704
988
|
emitTextDelta(getCallbacks, "1]");
|
|
705
989
|
emitMessageComplete(getCallbacks);
|
|
706
|
-
await waitFor(() => frames.some((frame) => frame.type === "
|
|
990
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
707
991
|
|
|
708
992
|
const joined = assistantDeltaTexts(frames).join("");
|
|
709
993
|
expect(joined).not.toContain("[-");
|
|
710
994
|
expect(ttsTexts.join(" ")).not.toContain("[-");
|
|
711
995
|
});
|
|
712
996
|
|
|
713
|
-
test("a mid-reply [-1]
|
|
997
|
+
test("a mid-reply [-1] is stripped from speech and never minimizes", async () => {
|
|
714
998
|
const { frames, session, getCallbacks } = createMarkerHarness();
|
|
715
999
|
|
|
716
1000
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -728,7 +1012,8 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
728
1012
|
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
729
1013
|
|
|
730
1014
|
await startReleasedTurn(session, getCallbacks);
|
|
731
|
-
|
|
1015
|
+
runUiTool(getCallbacks, "ui_show");
|
|
1016
|
+
emitTextDelta(getCallbacks, "Showing you now.");
|
|
732
1017
|
await flushAsyncCallbacks();
|
|
733
1018
|
|
|
734
1019
|
await session.handleClientFrame({ type: "interrupt" });
|
|
@@ -738,7 +1023,7 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
738
1023
|
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
739
1024
|
});
|
|
740
1025
|
|
|
741
|
-
test("a turn
|
|
1026
|
+
test("a turn that shows nothing emits no minimize_room frame", async () => {
|
|
742
1027
|
const { frames, session, getCallbacks } = createMarkerHarness();
|
|
743
1028
|
|
|
744
1029
|
await startReleasedTurn(session, getCallbacks);
|