@vellumai/assistant 0.11.2-dev.202608071820.0d9e564 → 0.11.2-dev.202608072015.e50ee9a

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.11.2-dev.202608071820.0d9e564",
3
+ "version": "0.11.2-dev.202608072015.e50ee9a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -406,18 +406,6 @@ export interface VoiceTurnHandle {
406
406
  * on a live phone call" framing (the session system prompt already
407
407
  * provides assistant identity) and guardian context (injected separately).
408
408
  */
409
- /**
410
- * Steering shared by every voice channel. A sign-in flow opens a browser
411
- * window mid-call that the caller may be unable to see or complete, whether it
412
- * is reached through a ui-surface tool or through shell and CLI tools (e.g.
413
- * `assistant oauth connect`). Tell the model to speak the limitation and defer
414
- * the flow to text chat instead.
415
- *
416
- * This outlives the ui-surface restriction it was written alongside: a
417
- * live-voice call can now show surfaces, but a browser window handing control
418
- * to a third party mid-call is a different problem, and one a minimized room
419
- * does not solve.
420
- */
421
409
  /**
422
410
  * How long a live-voice call waits on an approval before deciding it itself.
423
411
  *
@@ -428,8 +416,23 @@ export interface VoiceTurnHandle {
428
416
  */
429
417
  const VOICE_APPROVAL_TIMEOUT_MS = 45_000;
430
418
 
431
- export const VOICE_NO_SETUP_FLOWS_RULE =
432
- "Never start account connections, OAuth or sign-in flows, or any other action that opens a browser window or needs the user's screen during this call — not even through shell or CLI tools. If the task needs one, say so briefly and offer to finish it in text chat after the call.";
419
+ /**
420
+ * Telephony-only steering. A sign-in flow opens a browser window on a screen
421
+ * the caller does not have in front of them, whether it is reached through a
422
+ * ui-surface tool or through shell and CLI tools (e.g. `assistant oauth
423
+ * connect`). Tell the model to speak the limitation and defer the flow to text
424
+ * chat instead.
425
+ *
426
+ * Scoped to the phone because the screen is what decides it. A phone call has
427
+ * no screen, so the `open_url` signal a CLI tool can reach lands somewhere the
428
+ * caller will never see, and that signal bus carries no capability or
429
+ * conversation context, so this rule is the only thing standing in front of it
430
+ * here. A live-voice call is the opposite case: the user is holding the screen,
431
+ * and the room minimizes itself to hand it back (see
432
+ * LIVE_VOICE_SETUP_FLOW_TEACHING).
433
+ */
434
+ const PHONE_NO_SETUP_FLOWS_RULE =
435
+ "Never start account connections, OAuth or sign-in flows, or any other action that opens a browser window or needs the user's screen during this call, not even through shell or CLI tools. If the task needs one, say so briefly and offer to finish it in text chat after the call.";
433
436
 
434
437
  function buildVoiceCallControlPrompt(opts: {
435
438
  isInbound: boolean;
@@ -523,7 +526,7 @@ function buildVoiceCallControlPrompt(opts: {
523
526
  "9. After the opening greeting turn, treat the Task field as background context only — do not re-execute its instructions on subsequent turns.",
524
527
  '10. Do not make up information. If you are unsure, use [ASK_GUARDIAN: your question] to consult your guardian. For tool permission requests, use [ASK_GUARDIAN_APPROVAL: {"question":"...","toolName":"...","input":{...}}].',
525
528
  `11. Your text is sent directly to a text-to-speech engine. Never use markdown formatting (asterisks, headers, backticks, links) or emojis in your spoken responses. Write plain conversational text only. Protocol markers like ${opts.isCallerGuardian ? "[END_CALL]" : "[ASK_GUARDIAN: ...] and [END_CALL]"} are not spoken text and should still be used normally.`,
526
- `12. ${VOICE_NO_SETUP_FLOWS_RULE}`,
529
+ `12. ${PHONE_NO_SETUP_FLOWS_RULE}`,
527
530
  );
528
531
 
529
532
  // Triage-and-escalate routing rules. The front-door leg decides and may
@@ -4,7 +4,6 @@ import type {
4
4
  VoiceTurnCallbacks,
5
5
  VoiceTurnOptions,
6
6
  } from "../../calls/voice-session-bridge.js";
7
- import { VOICE_NO_SETUP_FLOWS_RULE } from "../../calls/voice-session-bridge.js";
8
7
  import type {
9
8
  StreamingTranscriber,
10
9
  SttStreamServerEvent,
@@ -254,14 +253,14 @@ describe("LiveVoiceSession archive and metrics events", () => {
254
253
  userMessageInterface: "macos",
255
254
  assistantMessageInterface: "macos",
256
255
  // Pins the full production control prompt for the FRONT-DOOR leg (the
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
- // room-minimize teaching is deliberately absent — only the escalated
261
- // leg learns it (see live-voice-triage-escalate.test.ts).
256
+ // first leg of every routed turn): the speech-first rule, and nothing
257
+ // else. This leg is toolless, so both the screen-reveal and setup-flow
258
+ // teachings are deliberately absent. It has nothing to show and no
259
+ // connection to run, and its capability digest already routes anything
260
+ // needing a tool to the escalated leg (see
261
+ // live-voice-triage-escalate.test.ts).
262
262
  voiceControlPrompt:
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
- VOICE_NO_SETUP_FLOWS_RULE,
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. ",
265
264
  });
266
265
  callbacks?.assistant_text_delta?.(makeTextDelta("Hello there."));
267
266
  callbacks?.message_complete?.(makeMessageComplete());
@@ -190,6 +190,35 @@ describe("live-voice triage-and-escalate routing", () => {
190
190
  expect(escalated?.content).toBe(ESCALATION_CONTINUATION_CONTENT);
191
191
  });
192
192
 
193
+ test("no leg is told to refuse setup flows, and the escalated leg is told to run them", async () => {
194
+ const { starter } = scriptedStartVoiceTurn({
195
+ frontDoor: ["[1] ", "Let me think about that."],
196
+ escalated: ["The detailed answer is 42."],
197
+ });
198
+ const { frames, session } = createHarness(starter);
199
+
200
+ await driveTurn(session);
201
+ await waitFor(() => starter.mock.calls.length >= 2);
202
+ await waitFor(() => frames.some((frame) => frame.type === "tts_done"));
203
+
204
+ const frontDoorPrompt =
205
+ starter.mock.calls[0]?.[0]?.voiceControlPrompt ?? "";
206
+ const escalatedPrompt =
207
+ starter.mock.calls[1]?.[0]?.voiceControlPrompt ?? "";
208
+ // The phone's no-setup-flows rule must not leak onto a channel that has a
209
+ // screen: it contradicts the base prompt's "never tell the user you cannot
210
+ // show them something".
211
+ expect(frontDoorPrompt).not.toContain("Never start account connections");
212
+ expect(escalatedPrompt).not.toContain("Never start account connections");
213
+ expect(frontDoorPrompt).not.toContain("finish it in text chat");
214
+ expect(escalatedPrompt).not.toContain("finish it in text chat");
215
+ // The leg that can actually run one is told to.
216
+ expect(escalatedPrompt).toContain("This includes connecting accounts");
217
+ // The toolless fast leg is not: a connection is not its to run, and its
218
+ // capability digest already routes anything needing a tool here.
219
+ expect(frontDoorPrompt).not.toContain("This includes connecting accounts");
220
+ });
221
+
193
222
  test("the screen-reveal teaching reaches the escalated leg's prompt but never the front-door leg's", async () => {
194
223
  const { starter } = scriptedStartVoiceTurn({
195
224
  frontDoor: ["[1] ", "Let me think about that."],
@@ -17,7 +17,6 @@ import type {
17
17
  import {
18
18
  getConversationTurnTeardown,
19
19
  resolveProcessingWaitMs,
20
- VOICE_NO_SETUP_FLOWS_RULE,
21
20
  waitForPriorTurnTeardown,
22
21
  } from "../calls/voice-session-bridge.js";
23
22
  import {
@@ -744,6 +743,20 @@ const LIVE_VOICE_CONTROL_PROMPT_BASE =
744
743
  const LIVE_VOICE_SCREEN_REVEAL_TEACHING =
745
744
  "The call renders as a full-screen overlay covering the app. Whenever you put something on screen, the overlay minimizes by itself as soon as you finish speaking, and the user is looking at what you made. So speak as if you are showing it to them right now (for example, close with something like: take a look), and never say you cannot show it, that this is a voice call, or that they should check it later. Never emit bracketed markers of any kind. ";
746
745
 
746
+ // The setup-flow case, spelled out because it is the one the model gets wrong
747
+ // on its own: connecting an account reads as something a call cannot do, so it
748
+ // declines and offers to do it later in text, which is the exact thing the base
749
+ // prompt above forbids.
750
+ //
751
+ // It can do it. The connect card is a ui surface like any other, so showing it
752
+ // minimizes the room on the same latch (`revealsUiSurface`), and the user is at
753
+ // the screen the browser window opens on. Appended alongside the screen-reveal
754
+ // teaching, to the same legs, for the same reason: the front-door leg is
755
+ // toolless, so a setup flow is not its to run, and its capability digest
756
+ // already tells it to escalate anything needing a tool rather than refuse.
757
+ const LIVE_VOICE_SETUP_FLOW_TEACHING =
758
+ "This includes connecting accounts. If a task needs an account connected or a sign-in completed, put the connection up on screen and let the user do it now. Do not decline it, and do not defer it to text chat. Say what you are connecting and that it is in front of them. ";
759
+
747
760
  // System-level guidance appended to a barge-in turn's control prompt so the
748
761
  // model treats the new utterance as a continuation of the request it was cut
749
762
  // off answering, rather than a fresh follow-up. Reaches the model only; it is
@@ -800,21 +813,21 @@ function buildLiveDeliveryNote(request: string, answer: string): string {
800
813
  return `The user interrupted you earlier, and in the background you finished ${what}. The call is still live and nobody is speaking, so tell them briefly that it is done and give them the result. Do not re-run any tool calls; the work is already complete, and do not repeat it verbatim if it no longer fits. What you produced was:\n\n${answer}`;
801
814
  }
802
815
 
803
- // Assemble a leg's model-facing control prompt: the base live-voice rules,
804
- // the screen-reveal teaching (withheld from the front-door leg, see
805
- // LIVE_VOICE_SCREEN_REVEAL_TEACHING), the shared no-setup-flows rule, plus
806
- // any pending barge-in merge context, completed-continuation context, and/or
807
- // the announcement instruction. A turn can carry several (a barge-in follow-up
808
- // that also has a continuation result waiting); the notes are model-only and
809
- // never render as user bubbles.
816
+ // Assemble a leg's model-facing control prompt: the base live-voice rules, the
817
+ // screen-reveal and setup-flow teaching (both withheld from the front-door leg,
818
+ // see LIVE_VOICE_SCREEN_REVEAL_TEACHING), plus any pending barge-in merge
819
+ // context, completed-continuation context, and/or the announcement instruction.
820
+ // A turn can carry several (a barge-in follow-up that also has a continuation
821
+ // result waiting); the notes are model-only and never render as user bubbles.
810
822
  function buildVoiceControlPrompt(
811
823
  turn: ActiveAssistantTurn,
812
824
  leg: { frontDoor?: boolean },
813
825
  ): string {
814
826
  let prompt =
815
827
  LIVE_VOICE_CONTROL_PROMPT_BASE +
816
- (leg.frontDoor === true ? "" : LIVE_VOICE_SCREEN_REVEAL_TEACHING) +
817
- VOICE_NO_SETUP_FLOWS_RULE;
828
+ (leg.frontDoor === true
829
+ ? ""
830
+ : LIVE_VOICE_SCREEN_REVEAL_TEACHING + LIVE_VOICE_SETUP_FLOW_TEACHING);
818
831
  if (turn.interruptedRequest) {
819
832
  prompt = `${prompt}\n\n${buildInterruptionMergeNote(turn.interruptedRequest)}`;
820
833
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Guards that `resolveOauthCallbackUrl` reaches plugins.
3
+ *
4
+ * The shim written into `<workspaceDir>/node_modules/@vellumai/plugin-api`
5
+ * re-binds whatever `Object.keys()` finds on the plugin-api namespace, so an
6
+ * export missing from `index.ts` is missing from every installed plugin with
7
+ * no other signal than a plugin failing to import it.
8
+ */
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import { PLUGIN_API_EXPORTS } from "../../embedded/plugin-api.js";
13
+ import * as pluginApi from "../index.js";
14
+
15
+ describe("plugin-api OAuth callback export", () => {
16
+ test("is exported from the plugin-api surface", () => {
17
+ expect(typeof pluginApi.resolveOauthCallbackUrl).toEqual("function");
18
+ });
19
+
20
+ test("is carried into the generated shim's binding list", () => {
21
+ expect(PLUGIN_API_EXPORTS).toContain("resolveOauthCallbackUrl");
22
+ });
23
+
24
+ test("takes no arguments, so a plugin cannot vary the URI", () => {
25
+ // The value a Client ID Metadata Document publishes and the value the
26
+ // host later authorizes with have to be the same string.
27
+ expect(pluginApi.resolveOauthCallbackUrl.length).toEqual(0);
28
+ });
29
+ });
@@ -180,6 +180,16 @@ export {
180
180
  // the same resolution as `webhooks register`, and registers the callback route
181
181
  // on the managed branches. The plugin defaults to the one in context.
182
182
  export { resolveWebhookUrl, type WebhookUrlOptions } from "./webhook-url.js";
183
+ // Resolve the redirect URI an authorization server should send a user back
184
+ // to after consent. One shared route serves every OAuth flow in the
185
+ // assistant, demultiplexed by OAuth `state`, so this takes no arguments and
186
+ // returns the same URL for every caller and every attempt. A plugin needs it
187
+ // when it publishes something that has to name the redirect URI ahead of the
188
+ // flow, such as a Client ID Metadata Document, whose `redirect_uris` an
189
+ // authorization server matches exactly. Throws when no public ingress is
190
+ // configured and the assistant is not connected to the platform, which is the
191
+ // case where no URL would work.
192
+ export { resolveOauthCallbackUrl } from "../inbound/oauth-callback-url.js";
183
193
  // Resolve a provider for a call site (optionally overriding the profile) so a
184
194
  // plugin can run inference through the workspace's configured profiles and
185
195
  // credentials — managed-proxy or BYOK — without supplying its own API key.