@vellumai/assistant 0.11.1 → 0.11.2-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +68 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +185 -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 +141 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +171 -41
- package/src/live-voice/protocol.ts +23 -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
|
@@ -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
|
|
|
@@ -157,13 +157,24 @@ async function waitFor(
|
|
|
157
157
|
function createCapturingTurnStarter(): {
|
|
158
158
|
startVoiceTurn: LiveVoiceTurnStarter;
|
|
159
159
|
getCallbacks: () => VoiceTurnCallbacks | undefined;
|
|
160
|
+
announceApprovalPending: () => void;
|
|
161
|
+
announceApprovalsResolved: () => void;
|
|
160
162
|
} {
|
|
161
163
|
let callbacks: VoiceTurnCallbacks | undefined;
|
|
164
|
+
let onApprovalPending: ((requestId: string) => void) | undefined;
|
|
165
|
+
let onApprovalsResolved: (() => void) | undefined;
|
|
162
166
|
const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
|
|
163
167
|
callbacks = options.callbacks;
|
|
168
|
+
onApprovalPending = options.onApprovalPending;
|
|
169
|
+
onApprovalsResolved = options.onApprovalsResolved;
|
|
164
170
|
return { turnId: "bridge-turn-1", abort: mock() };
|
|
165
171
|
});
|
|
166
|
-
return {
|
|
172
|
+
return {
|
|
173
|
+
startVoiceTurn,
|
|
174
|
+
getCallbacks: () => callbacks,
|
|
175
|
+
announceApprovalPending: () => onApprovalPending?.("req-1"),
|
|
176
|
+
announceApprovalsResolved: () => onApprovalsResolved?.(),
|
|
177
|
+
};
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
function createRecordingTtsStreamer(): {
|
|
@@ -442,7 +453,7 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
442
453
|
});
|
|
443
454
|
});
|
|
444
455
|
|
|
445
|
-
test("records tool_use_start on the active turn
|
|
456
|
+
test("records tool_use_start on the active turn and publishes what it is doing", async () => {
|
|
446
457
|
let callbacks: VoiceTurnCallbacks | undefined;
|
|
447
458
|
const startVoiceTurn = mock(async (options: VoiceTurnOptions) => {
|
|
448
459
|
callbacks = options.callbacks;
|
|
@@ -459,9 +470,8 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
459
470
|
"thinking",
|
|
460
471
|
]);
|
|
461
472
|
|
|
462
|
-
const frameCountBefore = frames.length;
|
|
463
473
|
callbacks?.tool_use_start?.("some_tool");
|
|
464
|
-
await
|
|
474
|
+
await waitForFrameCount(frames, 4);
|
|
465
475
|
|
|
466
476
|
const activeTurn = (
|
|
467
477
|
session as unknown as {
|
|
@@ -469,20 +479,30 @@ describe("LiveVoiceSession assistant turn", () => {
|
|
|
469
479
|
}
|
|
470
480
|
).activeAssistantTurn;
|
|
471
481
|
expect(activeTurn?.toolUseStarted).toBe(true);
|
|
472
|
-
|
|
482
|
+
// One activity frame, so a silent stretch of tool work is visible on the
|
|
483
|
+
// surfaces that show the session. An unrecognized tool still gets a line.
|
|
484
|
+
expect(frames.at(-1)).toMatchObject({
|
|
485
|
+
type: "activity",
|
|
486
|
+
label: "Working on it",
|
|
487
|
+
});
|
|
473
488
|
|
|
474
489
|
callbacks?.message_complete?.({
|
|
475
490
|
type: "message_complete",
|
|
476
491
|
conversationId: "conversation-123",
|
|
477
492
|
messageId: "assistant-message-123",
|
|
478
493
|
});
|
|
479
|
-
await waitForFrameCount(frames,
|
|
494
|
+
await waitForFrameCount(frames, 6);
|
|
480
495
|
expect(frames.map((frame) => frame.type)).toEqual([
|
|
481
496
|
"ready",
|
|
482
497
|
"stt_final",
|
|
483
498
|
"thinking",
|
|
499
|
+
"activity",
|
|
484
500
|
"tts_done",
|
|
501
|
+
// The turn is over, so the line clears: no surface should keep showing
|
|
502
|
+
// the last tool it touched through the silence that follows.
|
|
503
|
+
"activity",
|
|
485
504
|
]);
|
|
505
|
+
expect(frames.at(-1)).toMatchObject({ type: "activity", label: "" });
|
|
486
506
|
});
|
|
487
507
|
|
|
488
508
|
test("interrupt aborts the in-flight turn and ignores late bridge events", async () => {
|
|
@@ -609,7 +629,7 @@ describe("LiveVoiceSession tool-use spoken ack", () => {
|
|
|
609
629
|
});
|
|
610
630
|
});
|
|
611
631
|
|
|
612
|
-
describe("LiveVoiceSession
|
|
632
|
+
describe("LiveVoiceSession room reveal", () => {
|
|
613
633
|
function createMarkerHarness() {
|
|
614
634
|
const { startVoiceTurn, getCallbacks } = createCapturingTurnStarter();
|
|
615
635
|
const { streamTtsAudio, ttsTexts } = createRecordingTtsStreamer();
|
|
@@ -624,11 +644,10 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
624
644
|
}
|
|
625
645
|
|
|
626
646
|
/**
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* latch `minimizeRequested`.
|
|
647
|
+
* Harness for the leg that can actually put something on screen: routing
|
|
648
|
+
* fronts every turn with the fast leg, so the front-door call is scripted to
|
|
649
|
+
* escalate ("[1] Working on it.") and the returned callbacks drive the
|
|
650
|
+
* ESCALATED leg; the toolless front door can never run a ui tool.
|
|
632
651
|
*/
|
|
633
652
|
function createEscalatedMarkerHarness() {
|
|
634
653
|
let escalatedCallbacks: VoiceTurnCallbacks | undefined;
|
|
@@ -664,35 +683,175 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
664
683
|
};
|
|
665
684
|
}
|
|
666
685
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
686
|
+
/** Run a ui tool to completion, as the agent loop would. */
|
|
687
|
+
function runUiTool(
|
|
688
|
+
getCallbacks: () => VoiceTurnCallbacks | undefined,
|
|
689
|
+
toolName: string,
|
|
690
|
+
opts?: { isError?: boolean },
|
|
691
|
+
): void {
|
|
692
|
+
getCallbacks()?.tool_use_start?.(toolName);
|
|
693
|
+
getCallbacks()?.tool_result?.({
|
|
694
|
+
toolName,
|
|
695
|
+
resultPreview: "",
|
|
696
|
+
...(opts?.isError === true ? { isError: true } : {}),
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Showing a surface is what reveals the screen. Nothing the model says does,
|
|
701
|
+
// so the user never loses the reveal to a forgotten token.
|
|
702
|
+
test("a ui tool emits minimize_room after tts_done", async () => {
|
|
703
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
670
704
|
|
|
671
705
|
await startReleasedTurn(session, getCallbacks);
|
|
672
|
-
|
|
706
|
+
runUiTool(getCallbacks, "ui_show");
|
|
707
|
+
emitTextDelta(getCallbacks, "Here is the summary.");
|
|
673
708
|
emitMessageComplete(getCallbacks);
|
|
674
709
|
await waitFor(() => frames.some((frame) => frame.type === "minimize_room"));
|
|
675
710
|
|
|
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
711
|
const ttsDoneIndex = frames.findIndex((frame) => frame.type === "tts_done");
|
|
681
712
|
const minimizeIndex = frames.findIndex(
|
|
682
713
|
(frame) => frame.type === "minimize_room",
|
|
683
714
|
);
|
|
684
715
|
expect(ttsDoneIndex).toBeGreaterThanOrEqual(0);
|
|
716
|
+
// After the speech drains, never mid-sentence.
|
|
685
717
|
expect(minimizeIndex).toBeGreaterThan(ttsDoneIndex);
|
|
686
718
|
expect(frames[minimizeIndex]).toMatchObject({
|
|
687
719
|
type: "minimize_room",
|
|
688
720
|
turnId: "live-turn-1",
|
|
689
721
|
});
|
|
722
|
+
// At most once, however many surfaces the turn touched.
|
|
690
723
|
expect(
|
|
691
724
|
frames.filter((frame) => frame.type === "minimize_room"),
|
|
692
725
|
).toHaveLength(1);
|
|
693
726
|
});
|
|
694
727
|
|
|
695
|
-
|
|
728
|
+
// The reveal follows the surface, not the attempt: a rejected call (no
|
|
729
|
+
// surface_type, an empty card, a client that never acks) rendered nothing,
|
|
730
|
+
// and minimizing to show nothing is worse than staying put.
|
|
731
|
+
test("a failed ui call reveals nothing", async () => {
|
|
732
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
733
|
+
|
|
734
|
+
await startReleasedTurn(session, getCallbacks);
|
|
735
|
+
runUiTool(getCallbacks, "ui_show", { isError: true });
|
|
736
|
+
emitTextDelta(getCallbacks, "I could not show that.");
|
|
737
|
+
emitMessageComplete(getCallbacks);
|
|
738
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
739
|
+
|
|
740
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("a surface taken away before the reply ends reveals nothing", async () => {
|
|
744
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
745
|
+
|
|
746
|
+
await startReleasedTurn(session, getCallbacks);
|
|
747
|
+
runUiTool(getCallbacks, "ui_show");
|
|
748
|
+
runUiTool(getCallbacks, "ui_dismiss");
|
|
749
|
+
emitTextDelta(getCallbacks, "Never mind, sorted it.");
|
|
750
|
+
emitMessageComplete(getCallbacks);
|
|
751
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
752
|
+
|
|
753
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Opening an app puts something on screen just as a card does; a list keyed
|
|
757
|
+
// on "ui_" would have missed it.
|
|
758
|
+
test("opening an app reveals the screen", async () => {
|
|
759
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
760
|
+
|
|
761
|
+
await startReleasedTurn(session, getCallbacks);
|
|
762
|
+
runUiTool(getCallbacks, "app_open");
|
|
763
|
+
emitTextDelta(getCallbacks, "Opened it up.");
|
|
764
|
+
emitMessageComplete(getCallbacks);
|
|
765
|
+
await waitFor(() => frames.some((frame) => frame.type === "minimize_room"));
|
|
766
|
+
|
|
767
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(true);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// A blocked turn has no speech left to drain, and the card it is blocked on
|
|
771
|
+
// renders behind the room. Waiting for a drain that is not coming would
|
|
772
|
+
// leave the call silent in front of a decision the user cannot see.
|
|
773
|
+
test("a pending approval reveals the screen immediately", async () => {
|
|
774
|
+
const { startVoiceTurn, announceApprovalPending } =
|
|
775
|
+
createCapturingTurnStarter();
|
|
776
|
+
const { frames, session } = createSessionHarness({ startVoiceTurn });
|
|
777
|
+
|
|
778
|
+
await session.start();
|
|
779
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
780
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
781
|
+
|
|
782
|
+
announceApprovalPending();
|
|
783
|
+
await waitFor(() => frames.some((frame) => frame.type === "minimize_room"));
|
|
784
|
+
|
|
785
|
+
// Before any tts_done, unlike the drain-scoped reveal a shown surface gets.
|
|
786
|
+
expect(frames.some((frame) => frame.type === "tts_done")).toBe(false);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Every phrase progress narration has describes work in flight ("still on
|
|
790
|
+
// it", "almost there"). While the turn is blocked on the user, all of them
|
|
791
|
+
// are false, and the one that matters is the one about who is being waited
|
|
792
|
+
// on.
|
|
793
|
+
test("says it is waiting, once, and stops narrating progress", async () => {
|
|
794
|
+
const {
|
|
795
|
+
startVoiceTurn,
|
|
796
|
+
announceApprovalPending,
|
|
797
|
+
announceApprovalsResolved,
|
|
798
|
+
} = createCapturingTurnStarter();
|
|
799
|
+
const { streamTtsAudio, ttsTexts } = createRecordingTtsStreamer();
|
|
800
|
+
const { frames, session } = createSessionHarness({
|
|
801
|
+
startVoiceTurn,
|
|
802
|
+
streamTtsAudio,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
await session.start();
|
|
806
|
+
await session.handleClientFrame({ type: "ptt_release" });
|
|
807
|
+
await waitFor(() => frames.some((frame) => frame.type === "thinking"));
|
|
808
|
+
|
|
809
|
+
announceApprovalPending();
|
|
810
|
+
await waitFor(() => ttsTexts.join(" ").includes("okay"));
|
|
811
|
+
// Repeat announcements do not repeat the line.
|
|
812
|
+
announceApprovalPending();
|
|
813
|
+
await flushAsyncCallbacks();
|
|
814
|
+
|
|
815
|
+
const saidWaiting = ttsTexts.filter((text) => text.includes("okay")).length;
|
|
816
|
+
expect(saidWaiting).toBe(1);
|
|
817
|
+
|
|
818
|
+
announceApprovalsResolved();
|
|
819
|
+
await flushAsyncCallbacks();
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("dismissing a surface does not reveal the screen", async () => {
|
|
823
|
+
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
824
|
+
|
|
825
|
+
await startReleasedTurn(session, getCallbacks);
|
|
826
|
+
runUiTool(getCallbacks, "ui_dismiss");
|
|
827
|
+
emitTextDelta(getCallbacks, "Cleared that away.");
|
|
828
|
+
emitMessageComplete(getCallbacks);
|
|
829
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
830
|
+
|
|
831
|
+
// Minimizing to show the user something that is no longer there is the
|
|
832
|
+
// opposite of the point.
|
|
833
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// The marker is no longer taught to any leg, so this is about a model that
|
|
837
|
+
// emits one anyway: it must not be spoken, and it must not move the room.
|
|
838
|
+
test("a terminal [-1] is stripped from deltas and TTS and reveals nothing", async () => {
|
|
839
|
+
const { frames, session, getCallbacks, ttsTexts } =
|
|
840
|
+
createEscalatedMarkerHarness();
|
|
841
|
+
|
|
842
|
+
await startReleasedTurn(session, getCallbacks);
|
|
843
|
+
emitTextDelta(getCallbacks, "hello world. [-1]");
|
|
844
|
+
emitMessageComplete(getCallbacks);
|
|
845
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
846
|
+
|
|
847
|
+
const joined = assistantDeltaTexts(frames).join("");
|
|
848
|
+
expect(joined).toContain("hello world. ");
|
|
849
|
+
expect(joined).not.toContain("[-1]");
|
|
850
|
+
expect(ttsTexts.join(" ")).not.toContain("[-1]");
|
|
851
|
+
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test("holds a marker split across deltas and never leaks the partial", async () => {
|
|
696
855
|
const { frames, session, getCallbacks, ttsTexts } =
|
|
697
856
|
createEscalatedMarkerHarness();
|
|
698
857
|
|
|
@@ -703,14 +862,14 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
703
862
|
|
|
704
863
|
emitTextDelta(getCallbacks, "1]");
|
|
705
864
|
emitMessageComplete(getCallbacks);
|
|
706
|
-
await waitFor(() => frames.some((frame) => frame.type === "
|
|
865
|
+
await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
|
|
707
866
|
|
|
708
867
|
const joined = assistantDeltaTexts(frames).join("");
|
|
709
868
|
expect(joined).not.toContain("[-");
|
|
710
869
|
expect(ttsTexts.join(" ")).not.toContain("[-");
|
|
711
870
|
});
|
|
712
871
|
|
|
713
|
-
test("a mid-reply [-1]
|
|
872
|
+
test("a mid-reply [-1] is stripped from speech and never minimizes", async () => {
|
|
714
873
|
const { frames, session, getCallbacks } = createMarkerHarness();
|
|
715
874
|
|
|
716
875
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -728,7 +887,8 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
728
887
|
const { frames, session, getCallbacks } = createEscalatedMarkerHarness();
|
|
729
888
|
|
|
730
889
|
await startReleasedTurn(session, getCallbacks);
|
|
731
|
-
|
|
890
|
+
runUiTool(getCallbacks, "ui_show");
|
|
891
|
+
emitTextDelta(getCallbacks, "Showing you now.");
|
|
732
892
|
await flushAsyncCallbacks();
|
|
733
893
|
|
|
734
894
|
await session.handleClientFrame({ type: "interrupt" });
|
|
@@ -738,7 +898,7 @@ describe("LiveVoiceSession minimize-room marker", () => {
|
|
|
738
898
|
expect(frames.some((frame) => frame.type === "minimize_room")).toBe(false);
|
|
739
899
|
});
|
|
740
900
|
|
|
741
|
-
test("a turn
|
|
901
|
+
test("a turn that shows nothing emits no minimize_room frame", async () => {
|
|
742
902
|
const { frames, session, getCallbacks } = createMarkerHarness();
|
|
743
903
|
|
|
744
904
|
await startReleasedTurn(session, getCallbacks);
|
|
@@ -254,13 +254,13 @@ describe("LiveVoiceSession archive and metrics events", () => {
|
|
|
254
254
|
userMessageInterface: "macos",
|
|
255
255
|
assistantMessageInterface: "macos",
|
|
256
256
|
// Pins the full production control prompt for the FRONT-DOOR leg (the
|
|
257
|
-
// first leg of every routed turn): the
|
|
258
|
-
//
|
|
259
|
-
// no-setup-flows rule (no OAuth/browser flows mid-call). The [-1]
|
|
257
|
+
// first leg of every routed turn): the speech-first rule (which now
|
|
258
|
+
// permits a surface, since the room reveals one by itself) and the
|
|
259
|
+
// shared no-setup-flows rule (no OAuth/browser flows mid-call). The [-1]
|
|
260
260
|
// room-minimize teaching is deliberately absent — only the escalated
|
|
261
261
|
// leg learns it (see live-voice-triage-escalate.test.ts).
|
|
262
262
|
voiceControlPrompt:
|
|
263
|
-
"You are speaking in a local live voice session. Keep replies brief and conversational.
|
|
263
|
+
"You are speaking in a local live voice session. Keep replies brief and conversational. Speech is the main channel: say the answer, and do not narrate a surface instead of answering. You can also put something on screen when it genuinely helps (a form, a list to pick from, a progress card for long work); the call overlay minimizes by itself once you finish speaking, so the user sees it without doing anything. Never tell the user you cannot show them something. " +
|
|
264
264
|
VOICE_NO_SETUP_FLOWS_RULE,
|
|
265
265
|
});
|
|
266
266
|
callbacks?.assistant_text_delta?.(makeTextDelta("Hello there."));
|
|
@@ -190,7 +190,7 @@ describe("live-voice triage-and-escalate routing", () => {
|
|
|
190
190
|
expect(escalated?.content).toBe(ESCALATION_CONTINUATION_CONTENT);
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
-
test("the
|
|
193
|
+
test("the screen-reveal teaching reaches the escalated leg's prompt but never the front-door leg's", async () => {
|
|
194
194
|
const { starter } = scriptedStartVoiceTurn({
|
|
195
195
|
frontDoor: ["[1] ", "Let me think about that."],
|
|
196
196
|
escalated: ["The detailed answer is 42."],
|
|
@@ -205,10 +205,14 @@ describe("live-voice triage-and-escalate routing", () => {
|
|
|
205
205
|
starter.mock.calls[0]?.[0]?.voiceControlPrompt ?? "";
|
|
206
206
|
const escalatedPrompt =
|
|
207
207
|
starter.mock.calls[1]?.[0]?.voiceControlPrompt ?? "";
|
|
208
|
-
// The toolless fast leg has nothing to show
|
|
209
|
-
//
|
|
208
|
+
// The toolless fast leg has nothing to show, so it is never told the
|
|
209
|
+
// screen will be revealed.
|
|
210
|
+
expect(frontDoorPrompt).not.toContain("the overlay minimizes");
|
|
211
|
+
expect(escalatedPrompt).toContain("the overlay minimizes");
|
|
212
|
+
// No leg is taught a marker any more: the reveal is decided by whether a
|
|
213
|
+
// ui tool ran, never by anything the model emits.
|
|
210
214
|
expect(frontDoorPrompt).not.toContain("[-1]");
|
|
211
|
-
expect(escalatedPrompt).toContain("[-1]");
|
|
215
|
+
expect(escalatedPrompt).not.toContain("[-1]");
|
|
212
216
|
});
|
|
213
217
|
|
|
214
218
|
test("the verdict token and any text past the bridge cap never reach the transcript", async () => {
|
|
@@ -1207,8 +1207,16 @@ describe("LiveVoiceSession server VAD", () => {
|
|
|
1207
1207
|
});
|
|
1208
1208
|
// The barge-in utterance transcribes to nothing, so no follow-up turn
|
|
1209
1209
|
// consumes the stash before the interrupt lands.
|
|
1210
|
+
//
|
|
1211
|
+
// "third question" is repeated because finals are handed out by transcriber
|
|
1212
|
+
// creation order, and the session pre-arms a transcriber for the next
|
|
1213
|
+
// utterance. The interrupt below can discard that pre-armed one and re-arm,
|
|
1214
|
+
// which shifts the final utterance onto the following index — with a
|
|
1215
|
+
// 3-entry list it would fall off the end and transcribe to the "utterance
|
|
1216
|
+
// N" placeholder, so the wait for "third question" would time out rather
|
|
1217
|
+
// than fail on the assertion. Both landing spots carry the same text.
|
|
1210
1218
|
const { frames, session } = createHarness({
|
|
1211
|
-
finals: ["first question", "", "third question"],
|
|
1219
|
+
finals: ["first question", "", "third question", "third question"],
|
|
1212
1220
|
startVoiceTurn,
|
|
1213
1221
|
streamTtsAudio,
|
|
1214
1222
|
spawnBackgroundContinuation,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a tool call into one short user-facing line for the surfaces that show
|
|
3
|
+
* a running voice session.
|
|
4
|
+
*
|
|
5
|
+
* ## Why the copy lives here
|
|
6
|
+
*
|
|
7
|
+
* The iOS Live Activity has two drivers that must carry identical content: the
|
|
8
|
+
* live-voice socket, and an APNs push the daemon dispatches when the app is
|
|
9
|
+
* backgrounded and the web layer is suspended. Only one of them runs web code,
|
|
10
|
+
* so a label composed client-side could not be reproduced by the other. Both
|
|
11
|
+
* are therefore handed the same string, and this is where it is made.
|
|
12
|
+
*
|
|
13
|
+
* That does not contradict the rule that phase wording belongs to the web
|
|
14
|
+
* layer. The reason phase copy lives there is cadence: the iOS shell ships on
|
|
15
|
+
* App Store review while the web bundle deploys continuously, so a native
|
|
16
|
+
* `switch` would fossilize. This module deploys with the daemon, i.e.
|
|
17
|
+
* continuously, and it is the only layer that knows a tool ran at all.
|
|
18
|
+
*
|
|
19
|
+
* ## What makes a good label
|
|
20
|
+
*
|
|
21
|
+
* It is read on a Lock Screen, in a glance, by someone who is mid-conversation
|
|
22
|
+
* and not looking for detail. So: a present-participle phrase, no tool names,
|
|
23
|
+
* no arguments, no punctuation, and short enough to survive one line. Arguments
|
|
24
|
+
* are deliberately absent even though {@link summarizeToolInput} could supply
|
|
25
|
+
* them: a path or a URL on a Lock Screen is unreadable at a glance and can
|
|
26
|
+
* carry content the user would not choose to show a passer-by.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The tools that put something on screen worth looking at.
|
|
31
|
+
*
|
|
32
|
+
* `app_open` belongs here with the ui-surface tools: it proxies to a connected
|
|
33
|
+
* client to bring an app up, which is the same thing from the user's side, and
|
|
34
|
+
* a list keyed on "ui_" would have missed it.
|
|
35
|
+
*
|
|
36
|
+
* `ui_dismiss` is deliberately absent, and is handled as its own case: it
|
|
37
|
+
* retires a surface, and revealing the screen to show the user something that
|
|
38
|
+
* is no longer there is the opposite of the point.
|
|
39
|
+
*/
|
|
40
|
+
const UI_REVEAL_TOOLS: ReadonlySet<string> = new Set([
|
|
41
|
+
"ui_show",
|
|
42
|
+
"ui_update",
|
|
43
|
+
"app_open",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** The tool that takes a surface away again. */
|
|
47
|
+
const UI_DISMISS_TOOL = "ui_dismiss";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether running `toolName` leaves something on screen the user should be
|
|
51
|
+
* shown, which is what makes a live-voice turn reveal the screen behind the
|
|
52
|
+
* call overlay.
|
|
53
|
+
*/
|
|
54
|
+
export function revealsUiSurface(toolName: string): boolean {
|
|
55
|
+
return UI_REVEAL_TOOLS.has(toolName);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether `toolName` takes a surface off the screen. */
|
|
59
|
+
export function dismissesUiSurface(toolName: string): boolean {
|
|
60
|
+
return toolName === UI_DISMISS_TOOL;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Present-participle phrases for the built-in tools, by exact name.
|
|
65
|
+
*
|
|
66
|
+
* Grouped by what the user would say is happening, not by how the tool is
|
|
67
|
+
* implemented: `bash` and `host_bash` differ in where they run, which is not a
|
|
68
|
+
* distinction anyone glancing at a Lock Screen is making.
|
|
69
|
+
*/
|
|
70
|
+
const TOOL_ACTIVITY_LABELS: Readonly<Record<string, string>> = {
|
|
71
|
+
bash: "Running a command",
|
|
72
|
+
terminal: "Running a command",
|
|
73
|
+
host_bash: "Running a command",
|
|
74
|
+
file_read: "Reading a file",
|
|
75
|
+
host_file_read: "Reading a file",
|
|
76
|
+
file_write: "Writing a file",
|
|
77
|
+
host_file_write: "Writing a file",
|
|
78
|
+
file_edit: "Editing a file",
|
|
79
|
+
host_file_edit: "Editing a file",
|
|
80
|
+
host_file_transfer: "Moving a file",
|
|
81
|
+
file_search: "Searching files",
|
|
82
|
+
web_search: "Searching the web",
|
|
83
|
+
web_fetch: "Reading a page",
|
|
84
|
+
network_request: "Making a request",
|
|
85
|
+
browser: "Using the browser",
|
|
86
|
+
computer_use: "Using the computer",
|
|
87
|
+
memory: "Checking memory",
|
|
88
|
+
schedule: "Checking the schedule",
|
|
89
|
+
ui_show: "Putting something on screen",
|
|
90
|
+
ui_update: "Putting something on screen",
|
|
91
|
+
app_open: "Opening an app",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Prefix-matched fallbacks, tried in order when a tool has no exact entry.
|
|
96
|
+
*
|
|
97
|
+
* The tool vocabulary is open: plugins, MCP servers, and skills all contribute
|
|
98
|
+
* names this module has never seen, and a new built-in should not have to be
|
|
99
|
+
* registered here to avoid a blank island. Prefixes cover the families whose
|
|
100
|
+
* names are structured; {@link GENERIC_ACTIVITY_LABEL} covers the rest.
|
|
101
|
+
*/
|
|
102
|
+
const TOOL_ACTIVITY_LABEL_PREFIXES: ReadonlyArray<readonly [string, string]> = [
|
|
103
|
+
["browser_", "Using the browser"],
|
|
104
|
+
["computer_", "Using the computer"],
|
|
105
|
+
["file_", "Working with files"],
|
|
106
|
+
["host_file_", "Working with files"],
|
|
107
|
+
["web_", "Looking something up"],
|
|
108
|
+
["mcp_", "Using a connected app"],
|
|
109
|
+
["skill_", "Running a skill"],
|
|
110
|
+
["subagent", "Working on it"],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* What an unrecognized tool reads as.
|
|
115
|
+
*
|
|
116
|
+
* Deliberately vague rather than derived from the tool's name. A humanized
|
|
117
|
+
* `slack_conversations_history` is worse than "Working on it": it is longer
|
|
118
|
+
* than the slot, it leaks an internal vocabulary, and it reads like an error
|
|
119
|
+
* message. The label's job is to show the turn is alive and doing something,
|
|
120
|
+
* and this does that job for every tool that will ever exist.
|
|
121
|
+
*/
|
|
122
|
+
export const GENERIC_ACTIVITY_LABEL = "Working on it";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The line to show while `toolName` runs.
|
|
126
|
+
*
|
|
127
|
+
* Total, by construction: every tool gets a label, because the alternative is
|
|
128
|
+
* a surface that goes blank precisely when the assistant is busiest.
|
|
129
|
+
*/
|
|
130
|
+
export function activityLabelForTool(toolName: string): string {
|
|
131
|
+
const exact = TOOL_ACTIVITY_LABELS[toolName];
|
|
132
|
+
if (exact !== undefined) {
|
|
133
|
+
return exact;
|
|
134
|
+
}
|
|
135
|
+
for (const [prefix, label] of TOOL_ACTIVITY_LABEL_PREFIXES) {
|
|
136
|
+
if (toolName.startsWith(prefix)) {
|
|
137
|
+
return label;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return GENERIC_ACTIVITY_LABEL;
|
|
141
|
+
}
|