@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231715.8da971c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +42 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +97 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +69 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +9 -4
- package/src/calls/voice-escalation-judge.ts +7 -6
- package/src/calls/voice-leg-coordinator.ts +15 -4
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +50 -20
- package/src/calls/voice-triage-escalate.ts +61 -26
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +37 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +17 -14
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +13 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +116 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/conversation-types.ts +6 -2
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
* correct reply need something the front door cannot do from here? A
|
|
10
10
|
* confident yes overrules a front-door answer and hands the turn off.
|
|
11
11
|
*
|
|
12
|
-
* The judge
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The judge uses managed Jev by default when Vellum-managed inference is
|
|
13
|
+
* available. An explicit TypeSafe call-site override can use a user-owned
|
|
14
|
+
* connection. Every other outcome (unavailable route, provider error,
|
|
15
|
+
* timeout, unparseable answer) is a "clear" verdict, so the front door's own
|
|
16
|
+
* decision stands exactly as it would without the judge.
|
|
16
17
|
*/
|
|
17
18
|
|
|
19
|
+
import { askTypesafeNoul } from "../providers/jev/ask.js";
|
|
18
20
|
import type { Message, Provider } from "../providers/types.js";
|
|
19
21
|
import { safeStringSlice } from "../util/unicode.js";
|
|
20
|
-
import { askTypesafeNoul } from "./typesafe-noul.js";
|
|
21
22
|
|
|
22
23
|
export const VOICE_ESCALATION_JUDGE_CALL_SITE = "voiceEscalationJudge";
|
|
23
24
|
|
|
@@ -57,7 +58,7 @@ export interface EscalationJudgement {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const ESCALATION_QUESTION =
|
|
60
|
-
"The voice assistant answering right now has NO tools and no access to the user's accounts, files, messages, calendar,
|
|
61
|
+
"The voice assistant answering right now has NO tools and no access to the user's accounts, files, messages, calendar, apps, the web, or saved memories. It can start or refresh screen sharing and inspect shared screen images through built-in call controls. Answer no when the caller only asks to share their screen, look at their screen, or describe what is visible; obtaining a screen view does not require escalation. Requests to click, type, edit, or draw/highlight on the screen still require escalation. Would a correct reply to what the caller just said require doing something it cannot do from here: taking an action (sending, scheduling, creating, editing, changing, opening, calling), looking up live or current information, or retrieving a personal fact that is not already stated in the recent conversation? Answer yes if the caller is asking for, or agreeing to, any such action, even vaguely or via a short confirmation of an earlier offer. Answer no for conversation, opinions, general knowledge, and questions answerable from the recent conversation.";
|
|
61
62
|
|
|
62
63
|
function textOf(message: Message): string {
|
|
63
64
|
return message.content
|
|
@@ -33,7 +33,10 @@ import {
|
|
|
33
33
|
/** The phrase spoken across the hand-off, as {@link resolveSpokenEscalationBridge} settles it. */
|
|
34
34
|
export type SpokenEscalationBridge = ReturnType<
|
|
35
35
|
typeof resolveSpokenEscalationBridge
|
|
36
|
-
|
|
36
|
+
> & {
|
|
37
|
+
/** The answer path already emitted this text; only flush pending speech. */
|
|
38
|
+
alreadyReleased?: boolean;
|
|
39
|
+
};
|
|
37
40
|
|
|
38
41
|
/** The leg that answers for real after the front-door leg handed off. */
|
|
39
42
|
export interface EscalatedLeg {
|
|
@@ -169,8 +172,8 @@ export function createFrontDoorLegCoordinator(options: {
|
|
|
169
172
|
};
|
|
170
173
|
|
|
171
174
|
const handOff = (
|
|
172
|
-
|
|
173
|
-
opts: { overruled?: boolean } = {},
|
|
175
|
+
bridgeText: string,
|
|
176
|
+
opts: { overruled?: boolean; alreadyReleased?: boolean } = {},
|
|
174
177
|
): void => {
|
|
175
178
|
if (handedOff || !host.isLive()) {
|
|
176
179
|
return;
|
|
@@ -182,7 +185,13 @@ export function createFrontDoorLegCoordinator(options: {
|
|
|
182
185
|
} else {
|
|
183
186
|
host.abortLeg();
|
|
184
187
|
}
|
|
185
|
-
const bridge =
|
|
188
|
+
const bridge: SpokenEscalationBridge = opts.alreadyReleased
|
|
189
|
+
? {
|
|
190
|
+
spokenBridge: bridgeText,
|
|
191
|
+
usesFallback: false,
|
|
192
|
+
alreadyReleased: true,
|
|
193
|
+
}
|
|
194
|
+
: resolveSpokenEscalationBridge(bridgeText, host.language());
|
|
186
195
|
host.speakBridge(bridge);
|
|
187
196
|
// The bridge is the turn's spoken acknowledgement: narration keeps its
|
|
188
197
|
// minimum gap from it rather than following it back to back.
|
|
@@ -299,6 +308,8 @@ export function createFrontDoorLegCoordinator(options: {
|
|
|
299
308
|
const step = verdict.finish();
|
|
300
309
|
if (step.kind === "bridge") {
|
|
301
310
|
handOff(step.bridge);
|
|
311
|
+
} else if (step.kind === "terminal-escalate") {
|
|
312
|
+
handOff(step.bridge, { alreadyReleased: true });
|
|
302
313
|
}
|
|
303
314
|
}
|
|
304
315
|
return handedOff;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { shouldAttachHostProxyForCapability } from "../daemon/host-proxy-preactivation.js";
|
|
2
|
+
import { skillLoadTool } from "../tools/skills/load.js";
|
|
3
|
+
import type { ToolContext } from "../tools/types.js";
|
|
4
|
+
import { getLogger } from "../util/logger.js";
|
|
5
|
+
|
|
6
|
+
const log = getLogger("voice-screen-annotation");
|
|
7
|
+
|
|
8
|
+
export async function loadVoiceScreenAnnotation(
|
|
9
|
+
context: ToolContext,
|
|
10
|
+
): Promise<string | null> {
|
|
11
|
+
if (
|
|
12
|
+
!shouldAttachHostProxyForCapability(
|
|
13
|
+
"host_cu_annotate",
|
|
14
|
+
context.transportInterface,
|
|
15
|
+
context.sourceActorPrincipalId,
|
|
16
|
+
)
|
|
17
|
+
) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const result = await skillLoadTool.execute(
|
|
22
|
+
{ skill: "screen-annotation" },
|
|
23
|
+
context,
|
|
24
|
+
{ readOnly: true },
|
|
25
|
+
);
|
|
26
|
+
if (result.isError) {
|
|
27
|
+
log.warn(
|
|
28
|
+
{ conversationId: context.conversationId, error: result.content },
|
|
29
|
+
"Could not preload screen annotation",
|
|
30
|
+
);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return [
|
|
34
|
+
"The user is sharing their screen. skill_load has automatically loaded screen-annotation for this turn. Use its tools through skill_execute when pointing at the shared screen.",
|
|
35
|
+
result.content,
|
|
36
|
+
].join("\n\n");
|
|
37
|
+
} catch (err) {
|
|
38
|
+
log.warn(
|
|
39
|
+
{ err, conversationId: context.conversationId },
|
|
40
|
+
"Could not preload screen annotation",
|
|
41
|
+
);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -32,7 +32,9 @@ import { CONVERSATION_BUSY_MESSAGE } from "../daemon/conversation-messaging.js";
|
|
|
32
32
|
import type { ModeSessionSourceHandle } from "../daemon/conversation-mode-session.js";
|
|
33
33
|
import { resolveChannelCapabilities } from "../daemon/conversation-runtime-assembly.js";
|
|
34
34
|
import { getOrCreateConversation } from "../daemon/conversation-store.js";
|
|
35
|
+
import { getEffectiveEnabledPluginSet } from "../daemon/conversation-tool-setup.js";
|
|
35
36
|
import { preactivateHostProxySkills } from "../daemon/host-proxy-preactivation.js";
|
|
37
|
+
import { resolveTrustClass } from "../daemon/trust-context.js";
|
|
36
38
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
37
39
|
import {
|
|
38
40
|
newestPersistedSightFrame,
|
|
@@ -71,19 +73,21 @@ import {
|
|
|
71
73
|
import {
|
|
72
74
|
CALL_OPENING_MARKER,
|
|
73
75
|
CALL_VERIFICATION_COMPLETE_MARKER,
|
|
74
|
-
|
|
76
|
+
ESCALATE_VERDICT_TOKENS,
|
|
75
77
|
HOLD_VERDICT_TOKEN,
|
|
76
78
|
stripInternalSpeechMarkers,
|
|
77
79
|
terminalControlMarkerLength,
|
|
78
80
|
} from "./voice-control-protocol.js";
|
|
79
81
|
import { judgeEscalation } from "./voice-escalation-judge.js";
|
|
80
82
|
import type { VoiceEscalationTarget } from "./voice-escalation-target.js";
|
|
83
|
+
import { loadVoiceScreenAnnotation } from "./voice-screen-annotation.js";
|
|
81
84
|
import {
|
|
82
85
|
createFrontDoorStreamGate,
|
|
83
86
|
escalatedContinuationRule,
|
|
84
87
|
ESCALATION_CONTINUATION_CONTENT,
|
|
85
88
|
frontDoorCapabilityDigest,
|
|
86
89
|
frontDoorDecisionRule,
|
|
90
|
+
leadingEscalationToken,
|
|
87
91
|
spokenBridgeText,
|
|
88
92
|
type VoiceRoutingLeg,
|
|
89
93
|
} from "./voice-triage-escalate.js";
|
|
@@ -452,6 +456,8 @@ export interface VoiceTurnOptions {
|
|
|
452
456
|
* session reports `macos` for its channel capabilities, iOS included.
|
|
453
457
|
*/
|
|
454
458
|
macosDesktopSession?: boolean;
|
|
459
|
+
/** The desktop client currently shares a surface with this voice session. */
|
|
460
|
+
screenSharing?: boolean;
|
|
455
461
|
/** Whether this is an inbound call (no outbound task). */
|
|
456
462
|
isInbound: boolean;
|
|
457
463
|
/** The outbound call task, if any. */
|
|
@@ -835,30 +841,32 @@ function trimOuterTextEdges(blocks: ContentBlock[]): ContentBlock[] {
|
|
|
835
841
|
* `ESCALATE_VERDICT_TOKEN` reduces to a single text block holding the
|
|
836
842
|
* capped bridge; empty spoken text means the caller heard only the canned
|
|
837
843
|
* fallback bridge, which is audio-only and never a transcript row, so the
|
|
838
|
-
* caller should delete the row.
|
|
839
|
-
*
|
|
840
|
-
* from the persisted text to match.
|
|
844
|
+
* caller should delete the row. A terminal escalation keeps all speech
|
|
845
|
+
* already released before the verdict. Other stray verdict tokens were
|
|
846
|
+
* never spoken and are stripped from the persisted text to match.
|
|
841
847
|
*/
|
|
842
848
|
export function cutFrontDoorContentAtVerdict(
|
|
843
849
|
blocks: ContentBlock[],
|
|
844
850
|
): { blocks: ContentBlock[]; spokenText: string } | null {
|
|
845
851
|
const joinedText = joinedTextOfBlocks(blocks);
|
|
846
|
-
|
|
847
|
-
|
|
852
|
+
const spokenText = spokenBridgeText(joinedText);
|
|
853
|
+
if (
|
|
854
|
+
spokenText.length > 0 ||
|
|
855
|
+
leadingEscalationToken(joinedText) !== undefined
|
|
856
|
+
) {
|
|
848
857
|
return {
|
|
849
858
|
blocks: spokenText.length > 0 ? [{ type: "text", text: spokenText }] : [],
|
|
850
859
|
spokenText,
|
|
851
860
|
};
|
|
852
861
|
}
|
|
853
862
|
if (
|
|
854
|
-
!joinedText.includes(
|
|
863
|
+
!ESCALATE_VERDICT_TOKENS.some((token) => joinedText.includes(token)) &&
|
|
855
864
|
!joinedText.includes(HOLD_VERDICT_TOKEN)
|
|
856
865
|
) {
|
|
857
866
|
return null;
|
|
858
867
|
}
|
|
859
868
|
const kept = stripMarkersFromBlocks(blocks);
|
|
860
|
-
|
|
861
|
-
return { blocks: kept, spokenText };
|
|
869
|
+
return { blocks: kept, spokenText: joinedTextOfBlocks(kept).trim() };
|
|
862
870
|
}
|
|
863
871
|
|
|
864
872
|
// ---------------------------------------------------------------------------
|
|
@@ -1806,6 +1814,20 @@ export async function startVoiceTurn(
|
|
|
1806
1814
|
? createFrontDoorStreamGate(opts.unifiedVerdict === true)
|
|
1807
1815
|
: null;
|
|
1808
1816
|
|
|
1817
|
+
const broadcastFrontDoorText = (
|
|
1818
|
+
msg: Extract<AssistantEvent, { type: "assistant_text_delta" }>,
|
|
1819
|
+
): void => {
|
|
1820
|
+
// Answer text waits for the judge, including text flushed at completion.
|
|
1821
|
+
if (
|
|
1822
|
+
frontDoorStreamGate?.answering &&
|
|
1823
|
+
!escalationJudgeSettled &&
|
|
1824
|
+
hubHold === null
|
|
1825
|
+
) {
|
|
1826
|
+
hubHold = [];
|
|
1827
|
+
}
|
|
1828
|
+
emitHubEvent(msg);
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1809
1831
|
/**
|
|
1810
1832
|
* Broadcast one agent-loop event to hub subscribers, holding a front-door
|
|
1811
1833
|
* leg's control-plane text back at the boundary rather than emitting it and
|
|
@@ -1820,16 +1842,7 @@ export async function startVoiceTurn(
|
|
|
1820
1842
|
}
|
|
1821
1843
|
const released = frontDoorStreamGate.push(msg.text);
|
|
1822
1844
|
if (released.length > 0) {
|
|
1823
|
-
|
|
1824
|
-
// leg event after it, until the verdict says the caller hears it.
|
|
1825
|
-
if (
|
|
1826
|
-
frontDoorStreamGate.answering &&
|
|
1827
|
-
!escalationJudgeSettled &&
|
|
1828
|
-
hubHold === null
|
|
1829
|
-
) {
|
|
1830
|
-
hubHold = [];
|
|
1831
|
-
}
|
|
1832
|
-
emitHubEvent({ ...msg, text: released });
|
|
1845
|
+
broadcastFrontDoorText({ ...msg, text: released });
|
|
1833
1846
|
}
|
|
1834
1847
|
};
|
|
1835
1848
|
|
|
@@ -2108,6 +2121,23 @@ export async function startVoiceTurn(
|
|
|
2108
2121
|
sourceInterface,
|
|
2109
2122
|
sourceActorPrincipalId,
|
|
2110
2123
|
);
|
|
2124
|
+
if (opts.screenSharing === true) {
|
|
2125
|
+
const annotation = await loadVoiceScreenAnnotation({
|
|
2126
|
+
conversationId: opts.conversationId,
|
|
2127
|
+
workingDir: conversation.workingDir,
|
|
2128
|
+
trustClass: resolveTrustClass(opts.trustContext),
|
|
2129
|
+
transportInterface: sourceInterface,
|
|
2130
|
+
clientOs: "macos",
|
|
2131
|
+
enabledPluginSet: getEffectiveEnabledPluginSet(conversation),
|
|
2132
|
+
sourceActorPrincipalId,
|
|
2133
|
+
signal: opts.signal,
|
|
2134
|
+
});
|
|
2135
|
+
if (annotation !== null) {
|
|
2136
|
+
conversation.setVoiceCallControlPrompt(
|
|
2137
|
+
[voiceCallControlPrompt, annotation].filter(Boolean).join("\n\n"),
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2111
2141
|
}
|
|
2112
2142
|
await conversation.runAgentLoop(persistedContent, messageId, {
|
|
2113
2143
|
...(opts.subagentNotification?.cronRunId
|
|
@@ -2128,7 +2158,7 @@ export async function startVoiceTurn(
|
|
|
2128
2158
|
// never hands off, and correspondingly never flushes.
|
|
2129
2159
|
const trailing = frontDoorStreamGate.finish();
|
|
2130
2160
|
if (trailing.length > 0) {
|
|
2131
|
-
|
|
2161
|
+
broadcastFrontDoorText({
|
|
2132
2162
|
type: "assistant_text_delta",
|
|
2133
2163
|
text: trailing,
|
|
2134
2164
|
...(reservedAssistantRowId !== null
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* - `[0]` ({@link HOLD_VERDICT_TOKEN}, unified front-door only): the
|
|
8
8
|
* caller is mid-thought — the leg is discarded and listening continues.
|
|
9
|
-
* - `[
|
|
10
|
-
* holding phrase: the turn is too tricky
|
|
9
|
+
* - `[ESCALATE]` ({@link ESCALATE_VERDICT_TOKEN}) followed by ONE short natural
|
|
10
|
+
* holding phrase: the turn is too tricky. The phrase is spoken (capped
|
|
11
11
|
* at a single sentence) while the turn re-runs on the conversation's
|
|
12
12
|
* own profile, the model the caller's typed turns already run on.
|
|
13
13
|
* Because the holding phrase is spoken, the caller never hears the
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* the escalation hand-off: the bridge is capped session-side instead of
|
|
21
21
|
* trusting the model to stop. Every infra failure fails open to a normal
|
|
22
22
|
* committed answer turn.
|
|
23
|
+
* A standalone terminal `[ESCALATE]` is recovered at normal completion without
|
|
24
|
+
* delaying answer streaming or repeating the already released speech.
|
|
25
|
+
* The numeric `[1]` verdict is accepted only at the start of a reply.
|
|
23
26
|
*
|
|
24
27
|
* This module owns the routing policy in one place: the profile key, the
|
|
25
28
|
* leg-specific prompt rules, the leading-token classifier, and the bridge
|
|
@@ -32,13 +35,20 @@ import {
|
|
|
32
35
|
localizedOrDefault,
|
|
33
36
|
} from "../util/language-subtag.js";
|
|
34
37
|
import {
|
|
38
|
+
createControlMarkerHoldback,
|
|
35
39
|
ESCALATE_VERDICT_TOKEN,
|
|
40
|
+
ESCALATE_VERDICT_TOKENS,
|
|
36
41
|
HOLD_VERDICT_TOKEN,
|
|
37
42
|
stripInternalSpeechMarkers,
|
|
38
43
|
} from "./voice-control-protocol.js";
|
|
39
44
|
|
|
40
45
|
export { ESCALATE_VERDICT_TOKEN, HOLD_VERDICT_TOKEN };
|
|
41
46
|
|
|
47
|
+
export function leadingEscalationToken(text: string): string | undefined {
|
|
48
|
+
const leading = text.trimStart();
|
|
49
|
+
return ESCALATE_VERDICT_TOKENS.find((token) => leading.startsWith(token));
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
// The fast model fronting every turn is pinned by the `voiceFrontDoor` call
|
|
43
53
|
// site (see config/call-site-defaults.ts) — no per-turn profile override.
|
|
44
54
|
// The escalated leg is pinned by the bridge to the conversation's own
|
|
@@ -156,7 +166,7 @@ export const BRIDGE_SENTENCE_END_REGEX = new RegExp(
|
|
|
156
166
|
);
|
|
157
167
|
|
|
158
168
|
/**
|
|
159
|
-
* Normalize
|
|
169
|
+
* Normalize the post-verdict stream into the bridge that is actually
|
|
160
170
|
* spoken: internal markers stripped, cut just after the first sentence
|
|
161
171
|
* terminator, hard-capped at {@link MAX_ESCALATION_BRIDGE_CHARS}, trimmed.
|
|
162
172
|
* The session speaks exactly this, the persisted front-door row keeps
|
|
@@ -174,7 +184,7 @@ export function capEscalationBridge(rawBridge: string): string {
|
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
/**
|
|
177
|
-
* Whether enough of the post
|
|
187
|
+
* Whether enough of the post-verdict stream has arrived to finalize the
|
|
178
188
|
* bridge and hand off: a sentence terminator landed, or the hard cap is
|
|
179
189
|
* reached. Until then the session keeps buffering (the bridge is spoken in
|
|
180
190
|
* one piece at hand-off, so what is spoken is exactly the capped bridge).
|
|
@@ -188,24 +198,21 @@ export function isEscalationBridgeComplete(rawBridge: string): boolean {
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
/**
|
|
191
|
-
* The spoken bridge of a front-door leg's
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
* the
|
|
195
|
-
* reconstruct what the caller heard from a persisted row, by running the
|
|
196
|
-
* same verdict machine the live stream ran through.
|
|
201
|
+
* The spoken bridge of a front-door leg's full raw output: the capped
|
|
202
|
+
* phrase following a leading verdict, or the already released speech
|
|
203
|
+
* before a terminal verdict. Other output has no bridge. Transcript hygiene
|
|
204
|
+
* replays the same verdict machine to reconstruct what the caller heard.
|
|
197
205
|
*/
|
|
198
206
|
export function spokenBridgeText(frontDoorText: string): string {
|
|
199
207
|
const machine = createFrontDoorVerdictMachine(false);
|
|
200
208
|
const step = machine.push(frontDoorText);
|
|
201
|
-
if (step.kind
|
|
202
|
-
return "";
|
|
203
|
-
}
|
|
204
|
-
if (step.bridge !== null) {
|
|
209
|
+
if (step.kind === "escalate" && step.bridge !== null) {
|
|
205
210
|
return step.bridge;
|
|
206
211
|
}
|
|
207
212
|
const finished = machine.finish();
|
|
208
|
-
return finished.kind === "bridge"
|
|
213
|
+
return finished.kind === "bridge" || finished.kind === "terminal-escalate"
|
|
214
|
+
? finished.bridge
|
|
215
|
+
: "";
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
/**
|
|
@@ -221,7 +228,8 @@ export function needsFallbackBridge(frontDoorText: string): boolean {
|
|
|
221
228
|
/**
|
|
222
229
|
* Classification of a front-door leg's accumulated leading output (already
|
|
223
230
|
* `trimStart()`ed). `pending` means the stream could still become a verdict
|
|
224
|
-
* token
|
|
231
|
+
* token: keep buffering. Other results select the leg's streaming path;
|
|
232
|
+
* a terminal escalation verdict is checked separately at completion.
|
|
225
233
|
*/
|
|
226
234
|
export type FrontDoorLeadingVerdict =
|
|
227
235
|
| "pending"
|
|
@@ -250,12 +258,12 @@ export function classifyFrontDoorLeading(
|
|
|
250
258
|
if (holdEnabled && leading.startsWith(HOLD_VERDICT_TOKEN)) {
|
|
251
259
|
return "hold";
|
|
252
260
|
}
|
|
253
|
-
if (leading
|
|
261
|
+
if (leadingEscalationToken(leading) !== undefined) {
|
|
254
262
|
return "escalate";
|
|
255
263
|
}
|
|
256
264
|
const candidates = holdEnabled
|
|
257
|
-
? [HOLD_VERDICT_TOKEN,
|
|
258
|
-
:
|
|
265
|
+
? [HOLD_VERDICT_TOKEN, ...ESCALATE_VERDICT_TOKENS]
|
|
266
|
+
: ESCALATE_VERDICT_TOKENS;
|
|
259
267
|
if (candidates.some((token) => token.startsWith(leading))) {
|
|
260
268
|
return "pending";
|
|
261
269
|
}
|
|
@@ -287,6 +295,8 @@ export type FrontDoorStep =
|
|
|
287
295
|
| { readonly kind: "bridging" }
|
|
288
296
|
/** The capped bridge is complete: hand off to the escalated leg. */
|
|
289
297
|
| { readonly kind: "bridge"; readonly bridge: string }
|
|
298
|
+
/** A terminal verdict hands off using the answer text already released. */
|
|
299
|
+
| { readonly kind: "terminal-escalate"; readonly bridge: string }
|
|
290
300
|
/** The leg already held, handed off, or finished; nothing more happens. */
|
|
291
301
|
| { readonly kind: "done" };
|
|
292
302
|
|
|
@@ -299,8 +309,9 @@ export type FrontDoorStep =
|
|
|
299
309
|
*
|
|
300
310
|
* `push` feeds one delta and reports what it decided. `finish` is called
|
|
301
311
|
* when the leg completes normally: a bridge that stopped short of a
|
|
302
|
-
* sentence terminator hands off with what arrived. A
|
|
303
|
-
*
|
|
312
|
+
* sentence terminator hands off with what arrived. A standalone terminal
|
|
313
|
+
* escalation token also hands off, using the already released answer as
|
|
314
|
+
* its bridge. A cancelled leg never calls `finish`.
|
|
304
315
|
*/
|
|
305
316
|
export interface FrontDoorVerdictMachine {
|
|
306
317
|
push(deltaText: string): FrontDoorStep;
|
|
@@ -359,7 +370,9 @@ export function createFrontDoorVerdictMachine(
|
|
|
359
370
|
}
|
|
360
371
|
if (verdict === "escalate") {
|
|
361
372
|
stage = "bridging";
|
|
362
|
-
bridgeRaw = raw
|
|
373
|
+
bridgeRaw = raw
|
|
374
|
+
.trimStart()
|
|
375
|
+
.slice(leadingEscalationToken(raw)!.length);
|
|
363
376
|
return {
|
|
364
377
|
kind: "escalate",
|
|
365
378
|
bridge: isEscalationBridgeComplete(bridgeRaw)
|
|
@@ -379,6 +392,17 @@ export function createFrontDoorVerdictMachine(
|
|
|
379
392
|
if (stage === "bridging") {
|
|
380
393
|
return completeBridge();
|
|
381
394
|
}
|
|
395
|
+
if (stage === "answer") {
|
|
396
|
+
stage = "done";
|
|
397
|
+
const text = raw.trimEnd();
|
|
398
|
+
const beforeToken = text.slice(0, -ESCALATE_VERDICT_TOKEN.length);
|
|
399
|
+
if (text.endsWith(ESCALATE_VERDICT_TOKEN) && /\s$/.test(beforeToken)) {
|
|
400
|
+
return {
|
|
401
|
+
kind: "terminal-escalate",
|
|
402
|
+
bridge: stripInternalSpeechMarkers(beforeToken).trim(),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
382
406
|
stage = "done";
|
|
383
407
|
return DONE_STEP;
|
|
384
408
|
},
|
|
@@ -419,14 +443,25 @@ export interface FrontDoorStreamGate {
|
|
|
419
443
|
* `usesFallbackBridge` in `live-voice-session.ts`) and deletes the row
|
|
420
444
|
* rather than persisting a phrase the model never really produced, so
|
|
421
445
|
* there is no displayed text for the gate to agree with.
|
|
422
|
-
* - `answer`:
|
|
423
|
-
*
|
|
446
|
+
* - `answer`: speech streams immediately, with control markers held back
|
|
447
|
+
* and stripped using the same filter as the audio drivers.
|
|
424
448
|
*/
|
|
425
449
|
export function createFrontDoorStreamGate(
|
|
426
450
|
holdEnabled: boolean,
|
|
427
451
|
): FrontDoorStreamGate {
|
|
428
452
|
const machine = createFrontDoorVerdictMachine(holdEnabled);
|
|
429
453
|
let answering = false;
|
|
454
|
+
let rawSpeech = "";
|
|
455
|
+
let releasedSpeech = "";
|
|
456
|
+
const flushSpeech = createControlMarkerHoldback((text) => {
|
|
457
|
+
releasedSpeech += text;
|
|
458
|
+
});
|
|
459
|
+
const filterSpeech = (text: string, force = false): string => {
|
|
460
|
+
rawSpeech += text;
|
|
461
|
+
releasedSpeech = "";
|
|
462
|
+
flushSpeech(rawSpeech, { force });
|
|
463
|
+
return releasedSpeech;
|
|
464
|
+
};
|
|
430
465
|
const releasable = (bridge: string): string =>
|
|
431
466
|
bridge.length < MIN_SPOKEN_BRIDGE_CHARS ? "" : bridge;
|
|
432
467
|
const release = (step: FrontDoorStep): string => {
|
|
@@ -443,8 +478,8 @@ export function createFrontDoorStreamGate(
|
|
|
443
478
|
}
|
|
444
479
|
};
|
|
445
480
|
return {
|
|
446
|
-
push: (deltaText) => release(machine.push(deltaText)),
|
|
447
|
-
finish: () => release(machine.finish()),
|
|
481
|
+
push: (deltaText) => filterSpeech(release(machine.push(deltaText))),
|
|
482
|
+
finish: () => filterSpeech(release(machine.finish()), true),
|
|
448
483
|
get answering() {
|
|
449
484
|
return answering;
|
|
450
485
|
},
|
|
@@ -66,6 +66,20 @@ let platformInstallCalls: Array<{ name: string; force?: boolean }> = [];
|
|
|
66
66
|
let upgradePluginCalls: UpgradePluginOptions[] = [];
|
|
67
67
|
let catalogMatches: PluginSearchMatch[] = [];
|
|
68
68
|
let catalogError: Error | null = null;
|
|
69
|
+
const CATALOG_COMMIT = "a".repeat(40);
|
|
70
|
+
|
|
71
|
+
function githubCatalogMatch(name: string): PluginSearchMatch {
|
|
72
|
+
return {
|
|
73
|
+
name,
|
|
74
|
+
path: `github:example-org/${name}@${CATALOG_COMMIT}`,
|
|
75
|
+
category: null,
|
|
76
|
+
source: {
|
|
77
|
+
kind: "github",
|
|
78
|
+
repo: `example-org/${name}`,
|
|
79
|
+
ref: CATALOG_COMMIT,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
69
83
|
|
|
70
84
|
/**
|
|
71
85
|
* Queued daemon IPC responses. The default (empty queue) is a transport
|
|
@@ -269,7 +283,10 @@ beforeEach(() => {
|
|
|
269
283
|
installPluginCalls = [];
|
|
270
284
|
platformInstallCalls = [];
|
|
271
285
|
upgradePluginCalls = [];
|
|
272
|
-
catalogMatches = [
|
|
286
|
+
catalogMatches = [
|
|
287
|
+
githubCatalogMatch("example"),
|
|
288
|
+
githubCatalogMatch("imessage"),
|
|
289
|
+
];
|
|
273
290
|
catalogError = null;
|
|
274
291
|
ipcResults = [];
|
|
275
292
|
inspectResult = null;
|
|
@@ -485,6 +502,17 @@ describe("plugins install - declared-schedules consent", () => {
|
|
|
485
502
|
]);
|
|
486
503
|
expect(r.exitCode).toBe(0);
|
|
487
504
|
});
|
|
505
|
+
|
|
506
|
+
test("does not use the outage fallback for a gated integration", async () => {
|
|
507
|
+
catalogError = new Error("catalog unavailable");
|
|
508
|
+
|
|
509
|
+
const r = await runCommand(["plugins", "install", "gamma"]);
|
|
510
|
+
|
|
511
|
+
expect(installPluginCalls).toHaveLength(0);
|
|
512
|
+
expect(platformInstallCalls).toHaveLength(0);
|
|
513
|
+
expect(r.stderr).toContain("not in the marketplace catalog");
|
|
514
|
+
expect(r.exitCode).toBe(1);
|
|
515
|
+
});
|
|
488
516
|
});
|
|
489
517
|
|
|
490
518
|
describe("plugins upgrade - declared-schedules consent (local fallback)", () => {
|
|
@@ -44,7 +44,11 @@ Examples:
|
|
|
44
44
|
description:
|
|
45
45
|
"Destination address for outbound verification (handle, phone number, user ID, or email address)",
|
|
46
46
|
},
|
|
47
|
-
{
|
|
47
|
+
{
|
|
48
|
+
flags: "--rebind",
|
|
49
|
+
description:
|
|
50
|
+
"Create a code although the channel already has a guardian (see below)",
|
|
51
|
+
},
|
|
48
52
|
{
|
|
49
53
|
flags: "--conversation-id <conversationId>",
|
|
50
54
|
description: "Conversation ID for inbound challenges",
|
|
@@ -74,7 +78,13 @@ Routes between three creation modes based on the provided options:
|
|
|
74
78
|
2. Outbound: --channel <ch> --destination <dest>
|
|
75
79
|
Sends a verification code to the given destination. Supports telegram
|
|
76
80
|
(handle or chat ID), phone (E.164 number), slack and discord (user ID),
|
|
77
|
-
and email.
|
|
81
|
+
and email.
|
|
82
|
+
|
|
83
|
+
--rebind creates a code although the channel already has a guardian. On
|
|
84
|
+
telegram, slack, discord and email, only the account already bound can redeem
|
|
85
|
+
that code, which is how the same guardian verifies again. A code never moves
|
|
86
|
+
the guardian to a different account there: run "revoke" first, then create
|
|
87
|
+
without --rebind. On phone, an outbound code replaces the bound number.
|
|
78
88
|
|
|
79
89
|
3. Inbound: --channel <ch> (no --destination)
|
|
80
90
|
Generates a challenge secret for the guardian to send back on the
|
|
@@ -804,7 +804,8 @@ Examples:
|
|
|
804
804
|
$ assistant oauth request --provider google https://www.googleapis.com/calendar/v3/calendars/primary/events
|
|
805
805
|
$ assistant oauth request --provider slack -H "Content-Type: application/json" -d '{"channel":"C123"}' /api/chat.postMessage --json
|
|
806
806
|
$ assistant oauth request --provider google -X POST -H "Content-Type: multipart/related; boundary=b" -d @upload.txt "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"
|
|
807
|
-
$ assistant oauth request --provider google -X POST -H "Content-Type: application/pdf" -d @report.pdf "https://www.googleapis.com/upload/drive/v3/files?uploadType=media"
|
|
807
|
+
$ assistant oauth request --provider google -X POST -H "Content-Type: application/pdf" -d @report.pdf "https://www.googleapis.com/upload/drive/v3/files?uploadType=media"
|
|
808
|
+
$ assistant oauth request --provider google -X POST -H "Content-Type: message/rfc822" -d @draft.eml "https://www.googleapis.com/upload/gmail/v1/users/me/drafts?uploadType=media"`,
|
|
808
809
|
},
|
|
809
810
|
{
|
|
810
811
|
name: "disconnect",
|
|
@@ -929,9 +930,14 @@ Requests with unsupported headers, including If-Match and Idempotency-Key,
|
|
|
929
930
|
are rejected before calling the provider. A connection using your own OAuth
|
|
930
931
|
app can preserve those headers.
|
|
931
932
|
|
|
933
|
+
The printed base URL is a path prefix. Concatenating a provider path onto it
|
|
934
|
+
works, and so does a host-absolute path such as /upload/gmail/... or
|
|
935
|
+
/v1/charges: the gateway remaps that request onto the grant's prefix.
|
|
936
|
+
|
|
932
937
|
Examples:
|
|
933
938
|
$ assistant oauth proxy-url stripe_link
|
|
934
|
-
$ eval "$(assistant oauth proxy-url stripe_link --export)" && LINK_API_BASE_URL="$VELLUM_OAUTH_PROXY_BASE_URL" LINK_ACCESS_TOKEN="$VELLUM_OAUTH_PROXY_TOKEN" LINK_NO_REFRESH=1 link-cli payment-methods list --format json
|
|
939
|
+
$ eval "$(assistant oauth proxy-url stripe_link --export)" && LINK_API_BASE_URL="$VELLUM_OAUTH_PROXY_BASE_URL" LINK_ACCESS_TOKEN="$VELLUM_OAUTH_PROXY_TOKEN" LINK_NO_REFRESH=1 link-cli payment-methods list --format json
|
|
940
|
+
$ eval "$(assistant oauth proxy-url google --export)" && curl -X POST -H "Authorization: Bearer $VELLUM_OAUTH_PROXY_TOKEN" -H "Content-Type: message/rfc822" --data-binary @draft.eml "$VELLUM_OAUTH_PROXY_BASE_URL/upload/gmail/v1/users/me/drafts?uploadType=media"`,
|
|
935
941
|
},
|
|
936
942
|
],
|
|
937
943
|
};
|
|
@@ -309,6 +309,38 @@ describe("oauth request body encoding", () => {
|
|
|
309
309
|
},
|
|
310
310
|
]);
|
|
311
311
|
});
|
|
312
|
+
|
|
313
|
+
test("forwards a Gmail rfc822 draft upload URL and body", async () => {
|
|
314
|
+
const rfc822 =
|
|
315
|
+
"From: user@example.com\r\nTo: user@example.com\r\nSubject: Draft\r\n\r\nHello\r\n";
|
|
316
|
+
const { exitCode } = await runRequestCommand([
|
|
317
|
+
"--provider",
|
|
318
|
+
"google",
|
|
319
|
+
"-s",
|
|
320
|
+
"-X",
|
|
321
|
+
"POST",
|
|
322
|
+
"-H",
|
|
323
|
+
"Content-Type: message/rfc822",
|
|
324
|
+
"-d",
|
|
325
|
+
rfc822,
|
|
326
|
+
"https://www.googleapis.com/upload/gmail/v1/users/me/drafts?uploadType=media",
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
expect(exitCode).toBe(0);
|
|
330
|
+
expect(handleRequestCalls).toEqual([
|
|
331
|
+
{
|
|
332
|
+
body: {
|
|
333
|
+
provider: "google",
|
|
334
|
+
url: "https://www.googleapis.com/upload/gmail/v1/users/me/drafts?uploadType=media",
|
|
335
|
+
method: "POST",
|
|
336
|
+
headers: {
|
|
337
|
+
"Content-Type": "message/rfc822",
|
|
338
|
+
},
|
|
339
|
+
parsed_data: rfc822,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
312
344
|
});
|
|
313
345
|
|
|
314
346
|
describe("oauth request body output", () => {
|
|
@@ -55,7 +55,8 @@ Fields:
|
|
|
55
55
|
isPlatform Whether IS_PLATFORM is set (boolean)
|
|
56
56
|
baseUrl VELLUM_PLATFORM_URL — the platform gateway base URL
|
|
57
57
|
assistantId This assistant's platform UUID
|
|
58
|
-
hasAssistantApiKey Whether a stored assistant API key is available
|
|
58
|
+
hasAssistantApiKey Whether a stored assistant API key is available; null
|
|
59
|
+
when the credential store could not be read
|
|
59
60
|
hasWebhookSecret Whether a stored webhook secret is available (needed
|
|
60
61
|
for email and other inbound webhook channels)
|
|
61
62
|
available Whether callback registration prerequisites are satisfied
|
|
@@ -24,7 +24,7 @@ interface PlatformStatusResult {
|
|
|
24
24
|
isPlatform: boolean;
|
|
25
25
|
baseUrl: string;
|
|
26
26
|
assistantId: string;
|
|
27
|
-
hasAssistantApiKey: boolean;
|
|
27
|
+
hasAssistantApiKey: boolean | null;
|
|
28
28
|
hasWebhookSecret: boolean;
|
|
29
29
|
available: boolean;
|
|
30
30
|
organizationId: string | null;
|
|
@@ -95,7 +95,13 @@ export function registerPlatformCommand(program: Command): void {
|
|
|
95
95
|
log.info(`Base URL: ${result.baseUrl || "(not set)"}`);
|
|
96
96
|
log.info(`Assistant ID: ${result.assistantId || "(not set)"}`);
|
|
97
97
|
log.info(
|
|
98
|
-
`Assistant API key: ${
|
|
98
|
+
`Assistant API key: ${
|
|
99
|
+
result.hasAssistantApiKey === null
|
|
100
|
+
? "unknown (credential store unreachable)"
|
|
101
|
+
: result.hasAssistantApiKey
|
|
102
|
+
? "set"
|
|
103
|
+
: "not set"
|
|
104
|
+
}`,
|
|
99
105
|
);
|
|
100
106
|
log.info(
|
|
101
107
|
`Webhook secret: ${result.hasWebhookSecret ? "set" : "not set (run ensure-registration to provision)"}`,
|