@vellumai/assistant 0.4.23 → 0.4.26
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/bun.lock +3 -0
- package/package.json +2 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -15
- package/src/__tests__/assistant-events-sse-hardening.test.ts +9 -3
- package/src/__tests__/call-controller.test.ts +80 -0
- package/src/__tests__/config-schema.test.ts +38 -178
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +4 -1
- package/src/__tests__/credential-security-invariants.test.ts +0 -2
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +2 -2
- package/src/__tests__/ipc-snapshot.test.ts +0 -9
- package/src/__tests__/onboarding-template-contract.test.ts +10 -20
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/runtime-events-sse-parity.test.ts +10 -0
- package/src/__tests__/runtime-events-sse.test.ts +7 -0
- package/src/__tests__/session-runtime-assembly.test.ts +34 -8
- package/src/__tests__/system-prompt.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +12 -8
- package/src/__tests__/twilio-routes-twiml.test.ts +2 -2
- package/src/__tests__/twilio-routes.test.ts +2 -3
- package/src/__tests__/voice-quality.test.ts +21 -132
- package/src/calls/call-controller.ts +34 -29
- package/src/calls/relay-server.ts +11 -5
- package/src/calls/twilio-routes.ts +4 -38
- package/src/calls/voice-quality.ts +7 -63
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +7 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +144 -83
- package/src/config/bundled-skills/sms-setup/SKILL.md +0 -20
- package/src/config/bundled-skills/twilio-setup/SKILL.md +9 -17
- package/src/config/bundled-skills/voice-setup/SKILL.md +36 -1
- package/src/config/bundled-skills/voice-setup/icon.svg +20 -0
- package/src/config/calls-schema.ts +3 -53
- package/src/config/elevenlabs-schema.ts +33 -0
- package/src/config/schema.ts +183 -137
- package/src/config/types.ts +0 -1
- package/src/daemon/handlers/browser.ts +1 -6
- package/src/daemon/ipc-contract/browser.ts +5 -14
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/session-agent-loop-handlers.ts +3 -0
- package/src/daemon/session-runtime-assembly.ts +9 -7
- package/src/mcp/client.ts +2 -1
- package/src/memory/conversation-crud.ts +339 -166
- package/src/runtime/auth/middleware.ts +87 -26
- package/src/runtime/routes/events-routes.ts +7 -0
- package/src/runtime/routes/inbound-message-handler.ts +3 -4
- package/src/schedule/scheduler.ts +159 -45
- package/src/security/secure-keys.ts +3 -3
- package/src/tools/browser/browser-manager.ts +72 -228
- package/src/tools/browser/browser-screencast.ts +0 -5
- package/src/tools/network/script-proxy/certs.ts +7 -237
- package/src/tools/network/script-proxy/connect-tunnel.ts +1 -82
- package/src/tools/network/script-proxy/http-forwarder.ts +2 -151
- package/src/tools/network/script-proxy/logging.ts +12 -196
- package/src/tools/network/script-proxy/mitm-handler.ts +2 -270
- package/src/tools/network/script-proxy/policy.ts +4 -152
- package/src/tools/network/script-proxy/router.ts +2 -60
- package/src/tools/network/script-proxy/server.ts +5 -137
- package/src/tools/network/script-proxy/types.ts +19 -125
- package/src/tools/system/voice-config.ts +23 -1
- package/src/util/logger.ts +4 -1
- package/src/__tests__/elevenlabs-config.test.ts +0 -95
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +0 -407
- package/src/calls/elevenlabs-config.ts +0 -32
package/bun.lock
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
12
12
|
"@qdrant/js-client-rest": "^1.16.2",
|
|
13
13
|
"@sentry/node": "^10.38.0",
|
|
14
|
+
"@vellumai/proxy-sidecar": "file:../proxy-sidecar",
|
|
14
15
|
"agentmail": "^0.1.0",
|
|
15
16
|
"archiver": "^7.0.1",
|
|
16
17
|
"commander": "^13.1.0",
|
|
@@ -517,6 +518,8 @@
|
|
|
517
518
|
|
|
518
519
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.0", "", { "dependencies": { "@typescript-eslint/types": "8.56.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg=="],
|
|
519
520
|
|
|
521
|
+
"@vellumai/proxy-sidecar": ["@vellumai/proxy-sidecar@file:../proxy-sidecar", { "devDependencies": { "@types/bun": "^1.2.4", "@types/node": "^25.2.2", "typescript": "^5.7.3" } }],
|
|
522
|
+
|
|
520
523
|
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
|
521
524
|
|
|
522
525
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/assistant",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vellum": "./src/index.ts"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/claude-agent-sdk": "^0.2.42",
|
|
30
|
+
"@vellumai/proxy-sidecar": "file:../proxy-sidecar",
|
|
30
31
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
31
32
|
"@google/genai": "^1.40.0",
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
@@ -768,14 +768,6 @@ exports[`IPC message snapshots ClientMessage types oauth_connect_start serialize
|
|
|
768
768
|
}
|
|
769
769
|
`;
|
|
770
770
|
|
|
771
|
-
exports[`IPC message snapshots ClientMessage types browser_cdp_response serializes to expected JSON 1`] = `
|
|
772
|
-
{
|
|
773
|
-
"sessionId": "test-session",
|
|
774
|
-
"success": true,
|
|
775
|
-
"type": "browser_cdp_response",
|
|
776
|
-
}
|
|
777
|
-
`;
|
|
778
|
-
|
|
779
771
|
exports[`IPC message snapshots ClientMessage types work_items_list serializes to expected JSON 1`] = `
|
|
780
772
|
{
|
|
781
773
|
"status": "queued",
|
|
@@ -2455,13 +2447,6 @@ exports[`IPC message snapshots ServerMessage types oauth_connect_result serializ
|
|
|
2455
2447
|
}
|
|
2456
2448
|
`;
|
|
2457
2449
|
|
|
2458
|
-
exports[`IPC message snapshots ServerMessage types browser_cdp_request serializes to expected JSON 1`] = `
|
|
2459
|
-
{
|
|
2460
|
-
"sessionId": "test-session",
|
|
2461
|
-
"type": "browser_cdp_request",
|
|
2462
|
-
}
|
|
2463
|
-
`;
|
|
2464
|
-
|
|
2465
2450
|
exports[`IPC message snapshots ServerMessage types document_editor_show serializes to expected JSON 1`] = `
|
|
2466
2451
|
{
|
|
2467
2452
|
"initialContent": "# Hello World",
|
|
@@ -193,9 +193,15 @@ describe("SSE route — capacity limit", () => {
|
|
|
193
193
|
expect(res2.status).toBe(200);
|
|
194
194
|
expect(hub.subscriberCount()).toBe(1); // evicted 1, added 1
|
|
195
195
|
|
|
196
|
-
// First stream
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// First stream: the immediate heartbeat was enqueued during start(),
|
|
197
|
+
// then eviction closed the controller. Read past any buffered data
|
|
198
|
+
// until the stream signals done.
|
|
199
|
+
let evictDone = false;
|
|
200
|
+
while (!evictDone) {
|
|
201
|
+
const result = await reader1.read();
|
|
202
|
+
evictDone = result.done;
|
|
203
|
+
}
|
|
204
|
+
expect(evictDone).toBe(true);
|
|
199
205
|
|
|
200
206
|
ac2.abort();
|
|
201
207
|
});
|
|
@@ -2138,6 +2138,86 @@ describe("call-controller", () => {
|
|
|
2138
2138
|
controller.destroy();
|
|
2139
2139
|
});
|
|
2140
2140
|
|
|
2141
|
+
test('silence timeout suppressed during in-call guardian consultation (pendingGuardianInput)', async () => {
|
|
2142
|
+
mockSilenceTimeoutMs = 50; // Short timeout for testing
|
|
2143
|
+
mockConsultationTimeoutMs = 10_000; // Long enough to not interfere
|
|
2144
|
+
|
|
2145
|
+
// LLM emits an ASK_GUARDIAN marker so the controller creates a pendingGuardianInput
|
|
2146
|
+
mockStartVoiceTurn.mockImplementation(
|
|
2147
|
+
createMockVoiceTurn(["Let me check with your guardian. [ASK_GUARDIAN: Can this caller access the account?]"]),
|
|
2148
|
+
);
|
|
2149
|
+
const { relay, controller } = setupController();
|
|
2150
|
+
|
|
2151
|
+
// Trigger a turn that creates a pending guardian input request
|
|
2152
|
+
await controller.handleCallerUtterance("I need to access the account");
|
|
2153
|
+
// Allow turn to complete
|
|
2154
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2155
|
+
|
|
2156
|
+
// Verify a guardian input request is now pending
|
|
2157
|
+
expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
|
|
2158
|
+
// Relay state is still 'connected' (not 'awaiting_guardian_decision')
|
|
2159
|
+
expect(relay.mockConnectionState).toBe("connected");
|
|
2160
|
+
|
|
2161
|
+
// Clear any tokens from the turn itself
|
|
2162
|
+
relay.sentTokens.length = 0;
|
|
2163
|
+
|
|
2164
|
+
// Wait for the silence timeout to fire
|
|
2165
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2166
|
+
|
|
2167
|
+
// "Are you still there?" should NOT have been sent because
|
|
2168
|
+
// pendingGuardianInput is active
|
|
2169
|
+
const silenceTokens = relay.sentTokens.filter((t) =>
|
|
2170
|
+
t.token.includes("Are you still there?"),
|
|
2171
|
+
);
|
|
2172
|
+
expect(silenceTokens.length).toBe(0);
|
|
2173
|
+
|
|
2174
|
+
controller.destroy();
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
test('silence nudge resumes after guardian consultation resolves', async () => {
|
|
2178
|
+
mockSilenceTimeoutMs = 50; // Short timeout for testing
|
|
2179
|
+
mockConsultationTimeoutMs = 10_000; // Long enough to not interfere
|
|
2180
|
+
|
|
2181
|
+
// LLM emits an ASK_GUARDIAN marker so the controller creates a pendingGuardianInput
|
|
2182
|
+
mockStartVoiceTurn.mockImplementation(
|
|
2183
|
+
createMockVoiceTurn(["Let me check. [ASK_GUARDIAN: Is this approved?]"]),
|
|
2184
|
+
);
|
|
2185
|
+
const { relay, controller } = setupController();
|
|
2186
|
+
|
|
2187
|
+
// Trigger a turn that creates a pending guardian input request
|
|
2188
|
+
await controller.handleCallerUtterance("Can I do this?");
|
|
2189
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2190
|
+
|
|
2191
|
+
// Verify guardian input request is pending
|
|
2192
|
+
expect(controller.getPendingConsultationQuestionId()).not.toBeNull();
|
|
2193
|
+
|
|
2194
|
+
// Now resolve the consultation by providing an answer
|
|
2195
|
+
// Mock the next LLM turn for the answer-driven follow-up
|
|
2196
|
+
mockStartVoiceTurn.mockImplementation(
|
|
2197
|
+
createMockVoiceTurn(["Great news, your guardian approved the request."]),
|
|
2198
|
+
);
|
|
2199
|
+
await controller.handleUserAnswer("Yes, approved");
|
|
2200
|
+
// Allow the answer-driven turn to complete
|
|
2201
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2202
|
+
|
|
2203
|
+
// Guardian input request should now be cleared
|
|
2204
|
+
expect(controller.getPendingConsultationQuestionId()).toBeNull();
|
|
2205
|
+
|
|
2206
|
+
// Clear tokens from the answer turn
|
|
2207
|
+
relay.sentTokens.length = 0;
|
|
2208
|
+
|
|
2209
|
+
// Wait for the silence timeout to fire again
|
|
2210
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2211
|
+
|
|
2212
|
+
// "Are you still there?" SHOULD fire now that guardian wait is resolved
|
|
2213
|
+
const silenceTokens = relay.sentTokens.filter((t) =>
|
|
2214
|
+
t.token.includes("Are you still there?"),
|
|
2215
|
+
);
|
|
2216
|
+
expect(silenceTokens.length).toBe(1);
|
|
2217
|
+
|
|
2218
|
+
controller.destroy();
|
|
2219
|
+
});
|
|
2220
|
+
|
|
2141
2221
|
// ── Pointer message regression tests ─────────────────────────────
|
|
2142
2222
|
|
|
2143
2223
|
test("END_CALL marker writes completed pointer to origin conversation", async () => {
|
|
@@ -658,10 +658,10 @@ describe("AssistantConfigSchema", () => {
|
|
|
658
658
|
userConsultTimeoutSeconds: 120,
|
|
659
659
|
ttsPlaybackDelayMs: 3000,
|
|
660
660
|
accessRequestPollIntervalMs: 500,
|
|
661
|
-
guardianWaitUpdateInitialIntervalMs:
|
|
661
|
+
guardianWaitUpdateInitialIntervalMs: 15000,
|
|
662
662
|
guardianWaitUpdateInitialWindowMs: 30000,
|
|
663
|
-
guardianWaitUpdateSteadyMinIntervalMs:
|
|
664
|
-
guardianWaitUpdateSteadyMaxIntervalMs:
|
|
663
|
+
guardianWaitUpdateSteadyMinIntervalMs: 20000,
|
|
664
|
+
guardianWaitUpdateSteadyMaxIntervalMs: 30000,
|
|
665
665
|
disclosure: {
|
|
666
666
|
enabled: true,
|
|
667
667
|
text: 'At the very beginning of the call, introduce yourself as an assistant calling on behalf of the person you represent. Do not say "AI assistant".',
|
|
@@ -670,21 +670,8 @@ describe("AssistantConfigSchema", () => {
|
|
|
670
670
|
denyCategories: [],
|
|
671
671
|
},
|
|
672
672
|
voice: {
|
|
673
|
-
mode: "twilio_standard",
|
|
674
673
|
language: "en-US",
|
|
675
674
|
transcriptionProvider: "Deepgram",
|
|
676
|
-
fallbackToStandardOnError: true,
|
|
677
|
-
elevenlabs: {
|
|
678
|
-
voiceId: "",
|
|
679
|
-
voiceModelId: "",
|
|
680
|
-
speed: 1.0,
|
|
681
|
-
stability: 0.5,
|
|
682
|
-
similarityBoost: 0.75,
|
|
683
|
-
useSpeakerBoost: true,
|
|
684
|
-
agentId: "",
|
|
685
|
-
apiBaseUrl: "https://api.elevenlabs.io",
|
|
686
|
-
registerCallTimeoutMs: 5000,
|
|
687
|
-
},
|
|
688
675
|
},
|
|
689
676
|
callerIdentity: {
|
|
690
677
|
allowPerCallOverride: true,
|
|
@@ -789,43 +776,28 @@ describe("AssistantConfigSchema", () => {
|
|
|
789
776
|
|
|
790
777
|
test("config without calls.voice parses correctly and produces defaults", () => {
|
|
791
778
|
const result = AssistantConfigSchema.parse({});
|
|
792
|
-
expect(result.calls.voice.mode).toBe("twilio_standard");
|
|
793
779
|
expect(result.calls.voice.language).toBe("en-US");
|
|
794
780
|
expect(result.calls.voice.transcriptionProvider).toBe("Deepgram");
|
|
795
|
-
expect(result.calls.voice.fallbackToStandardOnError).toBe(true);
|
|
796
|
-
expect(result.calls.voice.elevenlabs.voiceId).toBe("");
|
|
797
|
-
expect(result.calls.voice.elevenlabs.voiceModelId).toBe("");
|
|
798
|
-
expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
|
|
799
|
-
expect(result.calls.voice.elevenlabs.stability).toBe(0.5);
|
|
800
|
-
expect(result.calls.voice.elevenlabs.similarityBoost).toBe(0.75);
|
|
801
|
-
expect(result.calls.voice.elevenlabs.useSpeakerBoost).toBe(true);
|
|
802
|
-
expect(result.calls.voice.elevenlabs.agentId).toBe("");
|
|
803
|
-
expect(result.calls.voice.elevenlabs.apiBaseUrl).toBe(
|
|
804
|
-
"https://api.elevenlabs.io",
|
|
805
|
-
);
|
|
806
|
-
expect(result.calls.voice.elevenlabs.registerCallTimeoutMs).toBe(5000);
|
|
807
781
|
});
|
|
808
782
|
|
|
809
|
-
test("
|
|
810
|
-
const result = AssistantConfigSchema.parse({
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
expect(
|
|
814
|
-
|
|
815
|
-
).toBeUndefined();
|
|
816
|
-
expect(result.calls.voice.elevenlabs.speed).toBe(1.0);
|
|
783
|
+
test("elevenlabs tuning params have correct defaults", () => {
|
|
784
|
+
const result = AssistantConfigSchema.parse({});
|
|
785
|
+
expect(result.elevenlabs.voiceModelId).toBe("");
|
|
786
|
+
expect(result.elevenlabs.speed).toBe(1.0);
|
|
787
|
+
expect(result.elevenlabs.stability).toBe(0.5);
|
|
788
|
+
expect(result.elevenlabs.similarityBoost).toBe(0.75);
|
|
817
789
|
});
|
|
818
790
|
|
|
819
|
-
test("rejects
|
|
791
|
+
test("rejects elevenlabs.speed below 0.7", () => {
|
|
820
792
|
const result = AssistantConfigSchema.safeParse({
|
|
821
|
-
|
|
793
|
+
elevenlabs: { speed: 0.5 },
|
|
822
794
|
});
|
|
823
795
|
expect(result.success).toBe(false);
|
|
824
796
|
});
|
|
825
797
|
|
|
826
|
-
test("rejects
|
|
798
|
+
test("rejects elevenlabs.speed above 1.2", () => {
|
|
827
799
|
const result = AssistantConfigSchema.safeParse({
|
|
828
|
-
|
|
800
|
+
elevenlabs: { speed: 1.5 },
|
|
829
801
|
});
|
|
830
802
|
expect(result.success).toBe(false);
|
|
831
803
|
});
|
|
@@ -834,37 +806,20 @@ describe("AssistantConfigSchema", () => {
|
|
|
834
806
|
const result = AssistantConfigSchema.parse({
|
|
835
807
|
calls: {
|
|
836
808
|
voice: {
|
|
837
|
-
mode: "twilio_elevenlabs_tts",
|
|
838
809
|
language: "es-ES",
|
|
839
810
|
transcriptionProvider: "Google",
|
|
840
|
-
fallbackToStandardOnError: false,
|
|
841
|
-
elevenlabs: {
|
|
842
|
-
voiceId: "abc123",
|
|
843
|
-
stability: 0.8,
|
|
844
|
-
},
|
|
845
811
|
},
|
|
846
812
|
},
|
|
813
|
+
elevenlabs: {
|
|
814
|
+
stability: 0.8,
|
|
815
|
+
},
|
|
847
816
|
});
|
|
848
|
-
expect(result.calls.voice.mode).toBe("twilio_elevenlabs_tts");
|
|
849
817
|
expect(result.calls.voice.language).toBe("es-ES");
|
|
850
818
|
expect(result.calls.voice.transcriptionProvider).toBe("Google");
|
|
851
|
-
expect(result.
|
|
852
|
-
expect(result.calls.voice.elevenlabs.voiceId).toBe("abc123");
|
|
853
|
-
expect(result.calls.voice.elevenlabs.stability).toBe(0.8);
|
|
819
|
+
expect(result.elevenlabs.stability).toBe(0.8);
|
|
854
820
|
// Defaults preserved for unset fields
|
|
855
|
-
expect(result.
|
|
856
|
-
expect(result.
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
test("rejects invalid calls.voice.mode", () => {
|
|
860
|
-
const result = AssistantConfigSchema.safeParse({
|
|
861
|
-
calls: { voice: { mode: "invalid_mode" } },
|
|
862
|
-
});
|
|
863
|
-
expect(result.success).toBe(false);
|
|
864
|
-
if (!result.success) {
|
|
865
|
-
const msgs = result.error.issues.map((i) => i.message);
|
|
866
|
-
expect(msgs.some((m) => m.includes("calls.voice.mode"))).toBe(true);
|
|
867
|
-
}
|
|
821
|
+
expect(result.elevenlabs.voiceModelId).toBe("");
|
|
822
|
+
expect(result.elevenlabs.similarityBoost).toBe(0.75);
|
|
868
823
|
});
|
|
869
824
|
|
|
870
825
|
test("rejects invalid calls.voice.transcriptionProvider", () => {
|
|
@@ -880,23 +835,9 @@ describe("AssistantConfigSchema", () => {
|
|
|
880
835
|
}
|
|
881
836
|
});
|
|
882
837
|
|
|
883
|
-
test("rejects
|
|
838
|
+
test("rejects elevenlabs.stability out of range", () => {
|
|
884
839
|
const result = AssistantConfigSchema.safeParse({
|
|
885
|
-
|
|
886
|
-
});
|
|
887
|
-
expect(result.success).toBe(false);
|
|
888
|
-
});
|
|
889
|
-
|
|
890
|
-
test("rejects calls.voice.elevenlabs.registerCallTimeoutMs below 1000", () => {
|
|
891
|
-
const result = AssistantConfigSchema.safeParse({
|
|
892
|
-
calls: { voice: { elevenlabs: { registerCallTimeoutMs: 500 } } },
|
|
893
|
-
});
|
|
894
|
-
expect(result.success).toBe(false);
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
test("rejects calls.voice.elevenlabs.registerCallTimeoutMs above 15000", () => {
|
|
898
|
-
const result = AssistantConfigSchema.safeParse({
|
|
899
|
-
calls: { voice: { elevenlabs: { registerCallTimeoutMs: 20000 } } },
|
|
840
|
+
elevenlabs: { stability: 1.5 },
|
|
900
841
|
});
|
|
901
842
|
expect(result.success).toBe(false);
|
|
902
843
|
});
|
|
@@ -972,114 +913,40 @@ describe("AssistantConfigSchema", () => {
|
|
|
972
913
|
// ---------------------------------------------------------------------------
|
|
973
914
|
|
|
974
915
|
describe("resolveVoiceQualityProfile", () => {
|
|
975
|
-
test("returns
|
|
916
|
+
test("always returns ElevenLabs ttsProvider", () => {
|
|
976
917
|
const config = AssistantConfigSchema.parse({});
|
|
977
918
|
const profile = resolveVoiceQualityProfile(config);
|
|
978
|
-
expect(profile.
|
|
979
|
-
expect(profile.ttsProvider).toBe("Google");
|
|
980
|
-
expect(profile.voice).toBe("Google.en-US-Journey-O");
|
|
919
|
+
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
981
920
|
expect(profile.transcriptionProvider).toBe("Deepgram");
|
|
982
|
-
expect(profile.fallbackToStandardOnError).toBe(true);
|
|
983
|
-
expect(profile.validationErrors).toEqual([]);
|
|
984
921
|
});
|
|
985
922
|
|
|
986
|
-
test("
|
|
923
|
+
test("uses shared elevenlabs.voiceId for voice", () => {
|
|
987
924
|
const config = AssistantConfigSchema.parse({
|
|
988
|
-
|
|
989
|
-
voice: {
|
|
990
|
-
mode: "twilio_elevenlabs_tts",
|
|
991
|
-
elevenlabs: { voiceId: "test-voice-id" },
|
|
992
|
-
},
|
|
993
|
-
},
|
|
925
|
+
elevenlabs: { voiceId: "test-voice-id" },
|
|
994
926
|
});
|
|
995
927
|
const profile = resolveVoiceQualityProfile(config);
|
|
996
|
-
expect(profile.mode).toBe("twilio_elevenlabs_tts");
|
|
997
928
|
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
998
929
|
expect(profile.voice).toBe("test-voice-id");
|
|
999
|
-
expect(profile.validationErrors).toEqual([]);
|
|
1000
930
|
});
|
|
1001
931
|
|
|
1002
|
-
test("
|
|
1003
|
-
const config = AssistantConfigSchema.parse({
|
|
1004
|
-
calls: {
|
|
1005
|
-
voice: {
|
|
1006
|
-
mode: "twilio_elevenlabs_tts",
|
|
1007
|
-
fallbackToStandardOnError: true,
|
|
1008
|
-
elevenlabs: { voiceId: "" },
|
|
1009
|
-
},
|
|
1010
|
-
},
|
|
1011
|
-
});
|
|
1012
|
-
const profile = resolveVoiceQualityProfile(config);
|
|
1013
|
-
expect(profile.mode).toBe("twilio_standard");
|
|
1014
|
-
expect(profile.ttsProvider).toBe("Google");
|
|
1015
|
-
expect(profile.voice).toBe("Google.en-US-Journey-O");
|
|
1016
|
-
expect(profile.validationErrors.length).toBe(1);
|
|
1017
|
-
expect(profile.validationErrors[0]).toContain("falling back");
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
test("returns errors for twilio_elevenlabs_tts with empty voiceId and fallback disabled", () => {
|
|
1021
|
-
const config = AssistantConfigSchema.parse({
|
|
1022
|
-
calls: {
|
|
1023
|
-
voice: {
|
|
1024
|
-
mode: "twilio_elevenlabs_tts",
|
|
1025
|
-
fallbackToStandardOnError: false,
|
|
1026
|
-
elevenlabs: { voiceId: "" },
|
|
1027
|
-
},
|
|
1028
|
-
},
|
|
1029
|
-
});
|
|
1030
|
-
const profile = resolveVoiceQualityProfile(config);
|
|
1031
|
-
expect(profile.mode).toBe("twilio_elevenlabs_tts");
|
|
1032
|
-
expect(profile.validationErrors.length).toBe(1);
|
|
1033
|
-
expect(profile.validationErrors[0]).toContain("voiceId is required");
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
test("returns correct profile for elevenlabs_agent with valid agentId", () => {
|
|
1037
|
-
const config = AssistantConfigSchema.parse({
|
|
1038
|
-
calls: {
|
|
1039
|
-
voice: {
|
|
1040
|
-
mode: "elevenlabs_agent",
|
|
1041
|
-
elevenlabs: { agentId: "agent-123", voiceId: "v1" },
|
|
1042
|
-
},
|
|
1043
|
-
},
|
|
1044
|
-
});
|
|
1045
|
-
const profile = resolveVoiceQualityProfile(config);
|
|
1046
|
-
expect(profile.mode).toBe("elevenlabs_agent");
|
|
1047
|
-
expect(profile.ttsProvider).toBe("ElevenLabs");
|
|
1048
|
-
expect(profile.voice).toBe("v1");
|
|
1049
|
-
expect(profile.agentId).toBe("agent-123");
|
|
1050
|
-
expect(profile.validationErrors).toEqual([]);
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
test("falls back for elevenlabs_agent with empty agentId and fallback enabled", () => {
|
|
1054
|
-
const config = AssistantConfigSchema.parse({
|
|
1055
|
-
calls: {
|
|
1056
|
-
voice: {
|
|
1057
|
-
mode: "elevenlabs_agent",
|
|
1058
|
-
fallbackToStandardOnError: true,
|
|
1059
|
-
elevenlabs: { agentId: "" },
|
|
1060
|
-
},
|
|
1061
|
-
},
|
|
1062
|
-
});
|
|
932
|
+
test("defaults to Rachel voice ID when elevenlabs.voiceId is not set", () => {
|
|
933
|
+
const config = AssistantConfigSchema.parse({});
|
|
1063
934
|
const profile = resolveVoiceQualityProfile(config);
|
|
1064
|
-
expect(profile.
|
|
1065
|
-
expect(profile.validationErrors.length).toBe(1);
|
|
1066
|
-
expect(profile.validationErrors[0]).toContain("agentId is empty");
|
|
935
|
+
expect(profile.voice).toBe("21m00Tcm4TlvDq8ikWAM");
|
|
1067
936
|
});
|
|
1068
937
|
|
|
1069
|
-
test("
|
|
938
|
+
test("applies voice tuning params from elevenlabs config", () => {
|
|
1070
939
|
const config = AssistantConfigSchema.parse({
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
940
|
+
elevenlabs: {
|
|
941
|
+
voiceId: "abc123",
|
|
942
|
+
voiceModelId: "turbo_v2_5",
|
|
943
|
+
speed: 0.9,
|
|
944
|
+
stability: 0.8,
|
|
945
|
+
similarityBoost: 0.9,
|
|
1077
946
|
},
|
|
1078
947
|
});
|
|
1079
948
|
const profile = resolveVoiceQualityProfile(config);
|
|
1080
|
-
expect(profile.
|
|
1081
|
-
expect(profile.validationErrors.length).toBe(1);
|
|
1082
|
-
expect(profile.validationErrors[0]).toContain("agentId is required");
|
|
949
|
+
expect(profile.voice).toBe("abc123-turbo_v2_5-0.9_0.8_0.9");
|
|
1083
950
|
});
|
|
1084
951
|
});
|
|
1085
952
|
|
|
@@ -1123,14 +990,9 @@ describe("buildElevenLabsVoiceSpec", () => {
|
|
|
1123
990
|
|
|
1124
991
|
test("default config uses a bare voiceId when no model override is set", () => {
|
|
1125
992
|
const config = AssistantConfigSchema.parse({
|
|
1126
|
-
|
|
1127
|
-
voice: {
|
|
1128
|
-
mode: "twilio_elevenlabs_tts",
|
|
1129
|
-
elevenlabs: { voiceId: "test" },
|
|
1130
|
-
},
|
|
1131
|
-
},
|
|
993
|
+
elevenlabs: { voiceId: "test" },
|
|
1132
994
|
});
|
|
1133
|
-
const spec = buildElevenLabsVoiceSpec(config.
|
|
995
|
+
const spec = buildElevenLabsVoiceSpec(config.elevenlabs);
|
|
1134
996
|
expect(spec).toBe("test");
|
|
1135
997
|
});
|
|
1136
998
|
});
|
|
@@ -1375,10 +1237,8 @@ describe("loadConfig with schema validation", () => {
|
|
|
1375
1237
|
expect(config.calls.userConsultTimeoutSeconds).toBe(120);
|
|
1376
1238
|
expect(config.calls.disclosure.enabled).toBe(true);
|
|
1377
1239
|
expect(config.calls.safety.denyCategories).toEqual([]);
|
|
1378
|
-
expect(config.calls.voice.mode).toBe("twilio_standard");
|
|
1379
1240
|
expect(config.calls.voice.language).toBe("en-US");
|
|
1380
1241
|
expect(config.calls.voice.transcriptionProvider).toBe("Deepgram");
|
|
1381
|
-
expect(config.calls.voice.elevenlabs.voiceId).toBe("");
|
|
1382
1242
|
expect(config.calls.model).toBeUndefined();
|
|
1383
1243
|
expect(config.calls.callerIdentity).toEqual({
|
|
1384
1244
|
allowPerCallOverride: true,
|
|
@@ -84,7 +84,10 @@ mock.module("../runtime/guardian-context-resolver.js", () => ({
|
|
|
84
84
|
trustClass: "guardian",
|
|
85
85
|
sourceChannel: "vellum",
|
|
86
86
|
}),
|
|
87
|
-
toGuardianRuntimeContext: (ctx: unknown) =>
|
|
87
|
+
toGuardianRuntimeContext: (sourceChannel: unknown, ctx: unknown) => ({
|
|
88
|
+
...(ctx as Record<string, unknown>),
|
|
89
|
+
sourceChannel,
|
|
90
|
+
}),
|
|
88
91
|
}));
|
|
89
92
|
|
|
90
93
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
@@ -226,7 +226,6 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
226
226
|
"tools/network/script-proxy/session-manager.ts", // proxy credential injection at runtime
|
|
227
227
|
"messaging/registry.ts", // checks stored credentials for connected providers
|
|
228
228
|
"calls/call-domain.ts", // caller identity resolution (user phone number lookup)
|
|
229
|
-
"calls/elevenlabs-config.ts", // ElevenLabs voice quality API key lookup
|
|
230
229
|
"calls/twilio-config.ts", // call infrastructure credential lookup
|
|
231
230
|
"calls/twilio-provider.ts", // call infrastructure credential lookup
|
|
232
231
|
"calls/twilio-rest.ts", // Twilio REST API credential lookup
|
|
@@ -234,7 +233,6 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
234
233
|
"runtime/http-server.ts", // HTTP server credential lookup
|
|
235
234
|
"daemon/handlers/twitter-auth.ts", // Twitter OAuth token storage
|
|
236
235
|
"twitter/oauth-client.ts", // Twitter OAuth API client (reads access token for API calls)
|
|
237
|
-
"calls/elevenlabs-config.ts", // ElevenLabs credential lookup
|
|
238
236
|
"cli/config-commands.ts", // CLI config management
|
|
239
237
|
"messaging/providers/telegram-bot/adapter.ts", // Telegram bot token lookup for connectivity check
|
|
240
238
|
"messaging/providers/sms/adapter.ts", // Twilio credential lookup for SMS connectivity check
|
|
@@ -119,13 +119,13 @@ describe("guardian-verify-setup skill — voice auto-followup", () => {
|
|
|
119
119
|
expect(pollingSection).toContain("Non-rebind flows");
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
test("polling is voice-only — does not apply to
|
|
122
|
+
test("polling is voice-only — does not apply to Telegram", () => {
|
|
123
123
|
const pollingSection =
|
|
124
124
|
skillContent
|
|
125
125
|
.split("## Voice Auto-Check Polling")[1]
|
|
126
126
|
?.split("## Step 6")[0] ?? "";
|
|
127
127
|
expect(pollingSection).toContain("voice-only");
|
|
128
|
-
expect(pollingSection).toContain("Do NOT poll for
|
|
128
|
+
expect(pollingSection).toContain("Do NOT poll for Telegram");
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
test('no instruction requires waiting for user to ask "did it work?"', () => {
|
|
@@ -476,11 +476,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
|
|
|
476
476
|
service: "gmail",
|
|
477
477
|
requestedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
478
478
|
},
|
|
479
|
-
browser_cdp_response: {
|
|
480
|
-
type: "browser_cdp_response",
|
|
481
|
-
sessionId: "test-session",
|
|
482
|
-
success: true,
|
|
483
|
-
},
|
|
484
479
|
work_items_list: {
|
|
485
480
|
type: "work_items_list",
|
|
486
481
|
status: "queued",
|
|
@@ -1592,10 +1587,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
|
|
|
1592
1587
|
grantedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
|
|
1593
1588
|
accountInfo: "user@example.com",
|
|
1594
1589
|
},
|
|
1595
|
-
browser_cdp_request: {
|
|
1596
|
-
type: "browser_cdp_request",
|
|
1597
|
-
sessionId: "test-session",
|
|
1598
|
-
},
|
|
1599
1590
|
document_editor_show: {
|
|
1600
1591
|
type: "document_editor_show",
|
|
1601
1592
|
sessionId: "sess-001",
|
|
@@ -14,11 +14,11 @@ describe("onboarding template contracts", () => {
|
|
|
14
14
|
expect(lower).toContain("who am i");
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
test("infers personality
|
|
17
|
+
test("infers personality organically instead of asking directly", () => {
|
|
18
18
|
const lower = bootstrap.toLowerCase();
|
|
19
|
-
// Personality step must instruct
|
|
19
|
+
// Personality step must instruct organic discovery via conversation
|
|
20
20
|
expect(lower).toContain("personality");
|
|
21
|
-
expect(lower).toContain("
|
|
21
|
+
expect(lower).toContain("emerge");
|
|
22
22
|
expect(lower).toContain("vibe");
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -41,15 +41,15 @@ describe("onboarding template contracts", () => {
|
|
|
41
41
|
const lower = bootstrap.toLowerCase();
|
|
42
42
|
// The template must prompt the assistant to ask about names.
|
|
43
43
|
expect(lower).toContain("name");
|
|
44
|
-
// The first step should be about
|
|
45
|
-
expect(lower).toContain("
|
|
44
|
+
// The first step should be about the assistant's name
|
|
45
|
+
expect(lower).toContain("your name");
|
|
46
46
|
// The conversation sequence must include identity/naming
|
|
47
47
|
expect(lower).toContain("who am i");
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
test("asks user name AFTER assistant identity is established", () => {
|
|
51
|
-
// Step 1 is
|
|
52
|
-
const assistantNameIdx = bootstrap.indexOf("
|
|
51
|
+
// Step 1 is the assistant's name, step 4 is asking the user's name
|
|
52
|
+
const assistantNameIdx = bootstrap.indexOf("Your name:");
|
|
53
53
|
const userNameIdx = bootstrap.indexOf("who am I talking to?");
|
|
54
54
|
expect(assistantNameIdx).toBeGreaterThan(-1);
|
|
55
55
|
expect(userNameIdx).toBeGreaterThan(-1);
|
|
@@ -87,10 +87,8 @@ describe("onboarding template contracts", () => {
|
|
|
87
87
|
expect(lower).toContain("home base");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
test("contains
|
|
90
|
+
test("contains refusal policy", () => {
|
|
91
91
|
const lower = bootstrap.toLowerCase();
|
|
92
|
-
// Must have a privacy section
|
|
93
|
-
expect(lower).toContain("privacy");
|
|
94
92
|
// Assistant name is hard-required, user details are best-effort
|
|
95
93
|
expect(lower).toContain("hard-required");
|
|
96
94
|
expect(lower).toContain("best-effort");
|
|
@@ -107,16 +105,8 @@ describe("onboarding template contracts", () => {
|
|
|
107
105
|
expect(lower).toContain("declined");
|
|
108
106
|
});
|
|
109
107
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
expect(lower).toContain("em dashes");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("preserves no technical jargon instruction", () => {
|
|
116
|
-
const lower = bootstrap.toLowerCase();
|
|
117
|
-
expect(lower).toContain("technical jargon");
|
|
118
|
-
expect(lower).toContain("system internals");
|
|
119
|
-
});
|
|
108
|
+
// em-dash and technical jargon instructions are now hardcoded in the system
|
|
109
|
+
// prompt builder (buildSystemPrompt) rather than in the BOOTSTRAP.md template.
|
|
120
110
|
|
|
121
111
|
test("preserves comment line format instruction", () => {
|
|
122
112
|
// The template must start with the comment format explanation
|
|
@@ -2276,7 +2276,7 @@ describe("relay-server", () => {
|
|
|
2276
2276
|
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
2277
2277
|
.filter((m) => m.type === "text");
|
|
2278
2278
|
const promptText = textMessages.map((m) => m.token ?? "").join("");
|
|
2279
|
-
expect(promptText).toContain("Hi, this is my
|
|
2279
|
+
expect(promptText).toContain("Hi, this is my human's assistant.");
|
|
2280
2280
|
expect(promptText).not.toContain("Vellum");
|
|
2281
2281
|
expect(promptText).toContain("don't recognize this number");
|
|
2282
2282
|
expect(promptText).toContain("Can I get your name");
|
|
@@ -2326,13 +2326,13 @@ describe("relay-server", () => {
|
|
|
2326
2326
|
// Should have transitioned to awaiting guardian decision
|
|
2327
2327
|
expect(relay.getConnectionState()).toBe("awaiting_guardian_decision");
|
|
2328
2328
|
|
|
2329
|
-
// Should have sent the hold message (guardian label defaults to "my
|
|
2329
|
+
// Should have sent the hold message (guardian label defaults to "my human")
|
|
2330
2330
|
const textMessages = ws.sentMessages
|
|
2331
2331
|
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
2332
2332
|
.filter((m) => m.type === "text");
|
|
2333
2333
|
expect(
|
|
2334
2334
|
textMessages.some((m) =>
|
|
2335
|
-
(m.token ?? "").includes("I've let my
|
|
2335
|
+
(m.token ?? "").includes("I've let my human know"),
|
|
2336
2336
|
),
|
|
2337
2337
|
).toBe(true);
|
|
2338
2338
|
expect(
|