@vellumai/assistant 0.5.7 → 0.5.8
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 +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +8 -0
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Unit tests for TwiML generation with voice quality profiles.
|
|
3
3
|
*
|
|
4
4
|
* Tests that generateTwiML correctly uses profile values for
|
|
5
|
-
* ttsProvider, voice, language,
|
|
5
|
+
* ttsProvider, voice, language, transcriptionProvider,
|
|
6
|
+
* and interruptSensitivity.
|
|
6
7
|
*/
|
|
7
8
|
import { describe, expect, mock, test } from "bun:test";
|
|
8
9
|
|
|
@@ -26,6 +27,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
26
27
|
transcriptionProvider: "Deepgram",
|
|
27
28
|
ttsProvider: "Google",
|
|
28
29
|
voice: "Google.en-US-Journey-O",
|
|
30
|
+
interruptSensitivity: "low",
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
expect(twiml).toContain('ttsProvider="Google"');
|
|
@@ -40,6 +42,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
40
42
|
transcriptionProvider: "Deepgram",
|
|
41
43
|
ttsProvider: "ElevenLabs",
|
|
42
44
|
voice: "voice123-turbo_v2_5-1_0.5_0.75",
|
|
45
|
+
interruptSensitivity: "low",
|
|
43
46
|
});
|
|
44
47
|
|
|
45
48
|
expect(twiml).toContain('ttsProvider="ElevenLabs"');
|
|
@@ -52,6 +55,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
52
55
|
transcriptionProvider: "Deepgram",
|
|
53
56
|
ttsProvider: "Google",
|
|
54
57
|
voice: "Google.en-US-Journey-O",
|
|
58
|
+
interruptSensitivity: "low",
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
expect(twiml).toContain('voice="Google.en-US-Journey-O"');
|
|
@@ -63,6 +67,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
63
67
|
transcriptionProvider: "Deepgram",
|
|
64
68
|
ttsProvider: "ElevenLabs",
|
|
65
69
|
voice: "abc123-turbo_v2_5-1_0.5_0.75",
|
|
70
|
+
interruptSensitivity: "low",
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
expect(twiml).toContain('voice="abc123-turbo_v2_5-1_0.5_0.75"');
|
|
@@ -74,6 +79,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
74
79
|
transcriptionProvider: "Google",
|
|
75
80
|
ttsProvider: "Google",
|
|
76
81
|
voice: "Google.es-MX-Standard-A",
|
|
82
|
+
interruptSensitivity: "low",
|
|
77
83
|
});
|
|
78
84
|
|
|
79
85
|
expect(twiml).toContain('language="es-MX"');
|
|
@@ -85,6 +91,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
85
91
|
transcriptionProvider: "Google",
|
|
86
92
|
ttsProvider: "Google",
|
|
87
93
|
voice: "Google.en-US-Journey-O",
|
|
94
|
+
interruptSensitivity: "low",
|
|
88
95
|
});
|
|
89
96
|
|
|
90
97
|
expect(twiml).toContain('transcriptionProvider="Google"');
|
|
@@ -96,6 +103,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
96
103
|
transcriptionProvider: "Deepgram",
|
|
97
104
|
ttsProvider: "Google",
|
|
98
105
|
voice: 'voice<>&"test',
|
|
106
|
+
interruptSensitivity: "low",
|
|
99
107
|
});
|
|
100
108
|
|
|
101
109
|
expect(twiml).toContain('voice="voice<>&"test"');
|
|
@@ -108,6 +116,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
108
116
|
transcriptionProvider: "Deepgram",
|
|
109
117
|
ttsProvider: "Google",
|
|
110
118
|
voice: "Google.en-US-Journey-O",
|
|
119
|
+
interruptSensitivity: "low",
|
|
111
120
|
});
|
|
112
121
|
|
|
113
122
|
expect(twiml).toContain(`callSessionId=${callSessionId}`);
|
|
@@ -119,6 +128,7 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
119
128
|
transcriptionProvider: "Deepgram",
|
|
120
129
|
ttsProvider: "Google",
|
|
121
130
|
voice: "Google.en-US-Journey-O",
|
|
131
|
+
interruptSensitivity: "low",
|
|
122
132
|
});
|
|
123
133
|
|
|
124
134
|
expect(twiml).toContain('interruptible="true"');
|
|
@@ -131,8 +141,136 @@ describe("generateTwiML with voice quality profile", () => {
|
|
|
131
141
|
transcriptionProvider: "Deepgram",
|
|
132
142
|
ttsProvider: "Google",
|
|
133
143
|
voice: "Google.en-US-Journey-O",
|
|
144
|
+
interruptSensitivity: "low",
|
|
134
145
|
});
|
|
135
146
|
|
|
136
147
|
expect(twiml).not.toContain("welcomeGreeting=");
|
|
137
148
|
});
|
|
149
|
+
|
|
150
|
+
test('TwiML includes interruptSensitivity="low" when profile has low', () => {
|
|
151
|
+
const twiml = generateTwiML(callSessionId, relayUrl, welcomeGreeting, {
|
|
152
|
+
language: "en-US",
|
|
153
|
+
transcriptionProvider: "Deepgram",
|
|
154
|
+
ttsProvider: "Google",
|
|
155
|
+
voice: "Google.en-US-Journey-O",
|
|
156
|
+
interruptSensitivity: "low",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(twiml).toContain('interruptSensitivity="low"');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("custom interruptSensitivity values are reflected correctly", () => {
|
|
163
|
+
const twimlMedium = generateTwiML(
|
|
164
|
+
callSessionId,
|
|
165
|
+
relayUrl,
|
|
166
|
+
welcomeGreeting,
|
|
167
|
+
{
|
|
168
|
+
language: "en-US",
|
|
169
|
+
transcriptionProvider: "Deepgram",
|
|
170
|
+
ttsProvider: "Google",
|
|
171
|
+
voice: "Google.en-US-Journey-O",
|
|
172
|
+
interruptSensitivity: "medium",
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(twimlMedium).toContain('interruptSensitivity="medium"');
|
|
177
|
+
|
|
178
|
+
const twimlHigh = generateTwiML(
|
|
179
|
+
callSessionId,
|
|
180
|
+
relayUrl,
|
|
181
|
+
welcomeGreeting,
|
|
182
|
+
{
|
|
183
|
+
language: "en-US",
|
|
184
|
+
transcriptionProvider: "Deepgram",
|
|
185
|
+
ttsProvider: "Google",
|
|
186
|
+
voice: "Google.en-US-Journey-O",
|
|
187
|
+
interruptSensitivity: "high",
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(twimlHigh).toContain('interruptSensitivity="high"');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("hints attribute present when hints string is non-empty", () => {
|
|
195
|
+
const twiml = generateTwiML(
|
|
196
|
+
callSessionId,
|
|
197
|
+
relayUrl,
|
|
198
|
+
welcomeGreeting,
|
|
199
|
+
{
|
|
200
|
+
language: "en-US",
|
|
201
|
+
transcriptionProvider: "Deepgram",
|
|
202
|
+
ttsProvider: "ElevenLabs",
|
|
203
|
+
voice: "voice123",
|
|
204
|
+
interruptSensitivity: "low",
|
|
205
|
+
},
|
|
206
|
+
undefined,
|
|
207
|
+
undefined,
|
|
208
|
+
"Alice,Bob,Vellum",
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(twiml).toContain('hints="Alice,Bob,Vellum"');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("hints attribute omitted when hints string is empty", () => {
|
|
215
|
+
const twiml = generateTwiML(
|
|
216
|
+
callSessionId,
|
|
217
|
+
relayUrl,
|
|
218
|
+
welcomeGreeting,
|
|
219
|
+
{
|
|
220
|
+
language: "en-US",
|
|
221
|
+
transcriptionProvider: "Deepgram",
|
|
222
|
+
ttsProvider: "ElevenLabs",
|
|
223
|
+
voice: "voice123",
|
|
224
|
+
interruptSensitivity: "low",
|
|
225
|
+
},
|
|
226
|
+
undefined,
|
|
227
|
+
undefined,
|
|
228
|
+
"",
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
expect(twiml).not.toContain("hints=");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("hints attribute omitted when hints parameter is undefined", () => {
|
|
235
|
+
const twiml = generateTwiML(
|
|
236
|
+
callSessionId,
|
|
237
|
+
relayUrl,
|
|
238
|
+
welcomeGreeting,
|
|
239
|
+
{
|
|
240
|
+
language: "en-US",
|
|
241
|
+
transcriptionProvider: "Deepgram",
|
|
242
|
+
ttsProvider: "ElevenLabs",
|
|
243
|
+
voice: "voice123",
|
|
244
|
+
interruptSensitivity: "low",
|
|
245
|
+
},
|
|
246
|
+
undefined,
|
|
247
|
+
undefined,
|
|
248
|
+
undefined,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(twiml).not.toContain("hints=");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("XML special characters in hints are escaped properly", () => {
|
|
255
|
+
const twiml = generateTwiML(
|
|
256
|
+
callSessionId,
|
|
257
|
+
relayUrl,
|
|
258
|
+
welcomeGreeting,
|
|
259
|
+
{
|
|
260
|
+
language: "en-US",
|
|
261
|
+
transcriptionProvider: "Deepgram",
|
|
262
|
+
ttsProvider: "ElevenLabs",
|
|
263
|
+
voice: "voice123",
|
|
264
|
+
interruptSensitivity: "low",
|
|
265
|
+
},
|
|
266
|
+
undefined,
|
|
267
|
+
undefined,
|
|
268
|
+
'O\'Brien,Smith & Jones,"Dr. Lee"',
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect(twiml).toContain(
|
|
272
|
+
'hints="O'Brien,Smith & Jones,"Dr. Lee""',
|
|
273
|
+
);
|
|
274
|
+
expect(twiml).not.toContain("hints=\"O'Brien");
|
|
275
|
+
});
|
|
138
276
|
});
|
|
@@ -124,4 +124,62 @@ describe("resolveVoiceQualityProfile", () => {
|
|
|
124
124
|
const profile = resolveVoiceQualityProfile();
|
|
125
125
|
expect(profile.voice).toBe("voice1-turbo_v2_5-0.9_0.8_0.9");
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
test("interruptSensitivity defaults to 'low' when not configured", () => {
|
|
129
|
+
mockConfig = {
|
|
130
|
+
elevenlabs: { voiceId: "abc" },
|
|
131
|
+
calls: {
|
|
132
|
+
voice: {
|
|
133
|
+
language: "en-US",
|
|
134
|
+
transcriptionProvider: "Deepgram",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
const profile = resolveVoiceQualityProfile();
|
|
139
|
+
expect(profile.interruptSensitivity).toBe("low");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("interruptSensitivity reflects configured value", () => {
|
|
143
|
+
mockConfig = {
|
|
144
|
+
elevenlabs: { voiceId: "abc" },
|
|
145
|
+
calls: {
|
|
146
|
+
voice: {
|
|
147
|
+
language: "en-US",
|
|
148
|
+
transcriptionProvider: "Deepgram",
|
|
149
|
+
interruptSensitivity: "high",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
const profile = resolveVoiceQualityProfile();
|
|
154
|
+
expect(profile.interruptSensitivity).toBe("high");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("hints defaults to empty array when not configured", () => {
|
|
158
|
+
mockConfig = {
|
|
159
|
+
elevenlabs: { voiceId: "abc" },
|
|
160
|
+
calls: {
|
|
161
|
+
voice: {
|
|
162
|
+
language: "en-US",
|
|
163
|
+
transcriptionProvider: "Deepgram",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const profile = resolveVoiceQualityProfile();
|
|
168
|
+
expect(profile.hints).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("hints reflects configured values", () => {
|
|
172
|
+
mockConfig = {
|
|
173
|
+
elevenlabs: { voiceId: "abc" },
|
|
174
|
+
calls: {
|
|
175
|
+
voice: {
|
|
176
|
+
language: "en-US",
|
|
177
|
+
transcriptionProvider: "Deepgram",
|
|
178
|
+
hints: ["Vellum", "Velissa", "AI assistant"],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const profile = resolveVoiceQualityProfile();
|
|
183
|
+
expect(profile.hints).toEqual(["Vellum", "Velissa", "AI assistant"]);
|
|
184
|
+
});
|
|
127
185
|
});
|
|
@@ -115,12 +115,14 @@ describe("016-migrate-credentials-from-keychain migration", () => {
|
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
test(
|
|
118
|
-
"
|
|
118
|
+
"throws when broker is not available (skips checkpoint for retry)",
|
|
119
119
|
async () => {
|
|
120
120
|
isAvailableFn.mockReturnValue(false);
|
|
121
121
|
|
|
122
|
-
//
|
|
123
|
-
await
|
|
122
|
+
// Throwing skips the checkpoint so the migration retries on next startup
|
|
123
|
+
await expect(
|
|
124
|
+
migrateCredentialsFromKeychainMigration.run(WORKSPACE_DIR),
|
|
125
|
+
).rejects.toThrow("Keychain broker not available after waiting");
|
|
124
126
|
|
|
125
127
|
// Should not proceed to list or migrate keys
|
|
126
128
|
expect(brokerListFn).not.toHaveBeenCalled();
|
package/src/acp/agent-process.ts
CHANGED
|
@@ -52,7 +52,7 @@ export class AcpAgentProcess {
|
|
|
52
52
|
|
|
53
53
|
this.proc = spawn(this.config.command, this.config.args, {
|
|
54
54
|
cwd,
|
|
55
|
-
stdio: ["pipe", "pipe", "
|
|
55
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
56
56
|
env: { ...process.env, ...this.config.env },
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -68,6 +68,14 @@ export class AcpAgentProcess {
|
|
|
68
68
|
stream,
|
|
69
69
|
);
|
|
70
70
|
|
|
71
|
+
// Capture stderr so agent crash details appear in logs
|
|
72
|
+
this.proc.stderr?.on("data", (chunk: Buffer) => {
|
|
73
|
+
const text = chunk.toString().trim();
|
|
74
|
+
if (text) {
|
|
75
|
+
log.error({ agentId: this.agentId, stderr: text }, "ACP agent stderr");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
71
79
|
// Handle process exit
|
|
72
80
|
this.proc.on("exit", (code) => {
|
|
73
81
|
this.handleProcessExit(code);
|
package/src/agent/loop.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface AgentLoopConfig {
|
|
|
23
23
|
maxTokens: number;
|
|
24
24
|
maxInputTokens?: number; // context window size for tool result truncation
|
|
25
25
|
thinking?: { enabled: boolean };
|
|
26
|
-
effort: "low" | "medium" | "high";
|
|
26
|
+
effort: "low" | "medium" | "high" | "max";
|
|
27
27
|
toolChoice?:
|
|
28
28
|
| { type: "auto" }
|
|
29
29
|
| { type: "any" }
|
|
@@ -40,6 +40,22 @@ const log = getLogger("guardian-request-resolvers");
|
|
|
40
40
|
// Helpers
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Determines whether a Slack delivery should use ephemeral mode.
|
|
45
|
+
*
|
|
46
|
+
* Ephemeral messages (`chat.postEphemeral`) require a real channel ID
|
|
47
|
+
* (starts with `C` for public/private channels, or `D` for DM conversations).
|
|
48
|
+
* When the chat ID is a user ID (starts with `U`), `chat.postEphemeral` fails
|
|
49
|
+
* with `channel_not_found`. In that case the message is already going to a DM
|
|
50
|
+
* opened by `chat.postMessage`, so ephemeral isn't needed.
|
|
51
|
+
*
|
|
52
|
+
* Returns `true` only when the source channel is Slack AND the chatId is a
|
|
53
|
+
* shared channel (starts with `C`), meaning other users could see the message.
|
|
54
|
+
*/
|
|
55
|
+
function shouldUseEphemeral(sourceChannel: string, chatId: string): boolean {
|
|
56
|
+
return sourceChannel === "slack" && chatId.startsWith("C");
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
// ---------------------------------------------------------------------------
|
|
44
60
|
// Types
|
|
45
61
|
// ---------------------------------------------------------------------------
|
|
@@ -372,13 +388,22 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
372
388
|
// Deliver denial notification and lifecycle signals when channel context is available
|
|
373
389
|
if (channelDeliveryContext) {
|
|
374
390
|
try {
|
|
391
|
+
const denialPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
392
|
+
chatId: requesterChatId,
|
|
393
|
+
text: "Your access request has been denied.",
|
|
394
|
+
assistantId,
|
|
395
|
+
};
|
|
396
|
+
// On Slack shared channels, deliver as ephemeral so only the requester sees the denial
|
|
397
|
+
if (
|
|
398
|
+
shouldUseEphemeral(channel, requesterChatId) &&
|
|
399
|
+
requesterExternalUserId
|
|
400
|
+
) {
|
|
401
|
+
denialPayload.ephemeral = true;
|
|
402
|
+
denialPayload.user = requesterExternalUserId;
|
|
403
|
+
}
|
|
375
404
|
await deliverChannelReply(
|
|
376
405
|
channelDeliveryContext.replyCallbackUrl,
|
|
377
|
-
|
|
378
|
-
chatId: requesterChatId,
|
|
379
|
-
text: "Your access request has been denied by the guardian.",
|
|
380
|
-
assistantId,
|
|
381
|
-
},
|
|
406
|
+
denialPayload,
|
|
382
407
|
channelDeliveryContext.bearerToken,
|
|
383
408
|
);
|
|
384
409
|
} catch (err) {
|
|
@@ -435,7 +460,7 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
435
460
|
desktopDeliverUrl,
|
|
436
461
|
{
|
|
437
462
|
chatId: targetChatId,
|
|
438
|
-
text: "Your access request has been denied
|
|
463
|
+
text: "Your access request has been denied.",
|
|
439
464
|
assistantId,
|
|
440
465
|
},
|
|
441
466
|
desktopBearerToken,
|
|
@@ -520,18 +545,28 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
520
545
|
let codeDelivered = true;
|
|
521
546
|
|
|
522
547
|
// Deliver verification code to guardian
|
|
548
|
+
const codeText =
|
|
549
|
+
`You approved access for ${requesterExternalUserId}. ` +
|
|
550
|
+
`Give them this verification code: \`${session.secret}\`. ` +
|
|
551
|
+
`The code expires in 10 minutes.`;
|
|
523
552
|
try {
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
553
|
+
const codePayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
554
|
+
chatId: channelDeliveryContext.guardianChatId,
|
|
555
|
+
text: codeText,
|
|
556
|
+
assistantId,
|
|
557
|
+
};
|
|
558
|
+
// On Slack shared channels, deliver the verification code as ephemeral
|
|
559
|
+
// so only the guardian sees the secret — not all channel members.
|
|
560
|
+
if (
|
|
561
|
+
shouldUseEphemeral(channel, channelDeliveryContext.guardianChatId) &&
|
|
562
|
+
ctx.actor.actorExternalUserId
|
|
563
|
+
) {
|
|
564
|
+
codePayload.ephemeral = true;
|
|
565
|
+
codePayload.user = ctx.actor.actorExternalUserId;
|
|
566
|
+
}
|
|
528
567
|
await deliverChannelReply(
|
|
529
568
|
channelDeliveryContext.replyCallbackUrl,
|
|
530
|
-
|
|
531
|
-
chatId: channelDeliveryContext.guardianChatId,
|
|
532
|
-
text: codeText,
|
|
533
|
-
assistantId,
|
|
534
|
-
},
|
|
569
|
+
codePayload,
|
|
535
570
|
channelDeliveryContext.bearerToken,
|
|
536
571
|
);
|
|
537
572
|
} catch (err) {
|
|
@@ -542,20 +577,85 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
542
577
|
codeDelivered = false;
|
|
543
578
|
}
|
|
544
579
|
|
|
545
|
-
//
|
|
546
|
-
|
|
580
|
+
// If the guardian approved in a shared channel (not a DM), also send
|
|
581
|
+
// them a DM with the verification code for better privacy and
|
|
582
|
+
// discoverability. On Slack, posting to a user ID opens a DM.
|
|
583
|
+
const guardianUserId = ctx.actor.actorExternalUserId;
|
|
584
|
+
if (
|
|
585
|
+
codeDelivered &&
|
|
586
|
+
channel === "slack" &&
|
|
587
|
+
guardianUserId &&
|
|
588
|
+
!channelDeliveryContext.guardianChatId.startsWith("D")
|
|
589
|
+
) {
|
|
590
|
+
// Strip threadTs from the callback URL — it belongs to the shared
|
|
591
|
+
// channel thread and would cause thread_not_found errors in the DM.
|
|
592
|
+
let dmCallbackUrl = channelDeliveryContext.replyCallbackUrl;
|
|
593
|
+
try {
|
|
594
|
+
const url = new URL(channelDeliveryContext.replyCallbackUrl);
|
|
595
|
+
url.searchParams.delete("threadTs");
|
|
596
|
+
dmCallbackUrl = url.toString();
|
|
597
|
+
} catch {
|
|
598
|
+
// Malformed URL — use as-is
|
|
599
|
+
}
|
|
600
|
+
|
|
547
601
|
try {
|
|
548
602
|
await deliverChannelReply(
|
|
549
|
-
|
|
603
|
+
dmCallbackUrl,
|
|
550
604
|
{
|
|
551
|
-
chatId:
|
|
552
|
-
text:
|
|
553
|
-
"Your access request has been approved! " +
|
|
554
|
-
"Please enter the 6-digit verification code you receive from the guardian.",
|
|
605
|
+
chatId: guardianUserId,
|
|
606
|
+
text: codeText,
|
|
555
607
|
assistantId,
|
|
556
608
|
},
|
|
557
609
|
channelDeliveryContext.bearerToken,
|
|
558
610
|
);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
// Best-effort: the code was already delivered in the shared channel
|
|
613
|
+
log.warn(
|
|
614
|
+
{ err, guardianUserId },
|
|
615
|
+
"Failed to send guardian DM confirmation with verification code",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Notify the requester. For Slack, route to DM via the user ID and
|
|
621
|
+
// strip threadTs (which belongs to the guardian's channel thread).
|
|
622
|
+
const requesterTargetChatId =
|
|
623
|
+
channel === "slack" && requesterExternalUserId
|
|
624
|
+
? requesterExternalUserId
|
|
625
|
+
: requesterChatId;
|
|
626
|
+
let requesterCallbackUrl = channelDeliveryContext.replyCallbackUrl;
|
|
627
|
+
if (channel === "slack" && requesterExternalUserId) {
|
|
628
|
+
try {
|
|
629
|
+
const url = new URL(channelDeliveryContext.replyCallbackUrl);
|
|
630
|
+
url.searchParams.delete("threadTs");
|
|
631
|
+
requesterCallbackUrl = url.toString();
|
|
632
|
+
} catch {
|
|
633
|
+
// Malformed URL — use as-is
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (codeDelivered) {
|
|
638
|
+
try {
|
|
639
|
+
const approvalPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
640
|
+
chatId: requesterTargetChatId,
|
|
641
|
+
text:
|
|
642
|
+
"Your access request has been approved! " +
|
|
643
|
+
"Please enter the 6-digit verification code you receive from the guardian.",
|
|
644
|
+
assistantId,
|
|
645
|
+
};
|
|
646
|
+
// On Slack shared channels, deliver as ephemeral so only the requester sees
|
|
647
|
+
if (
|
|
648
|
+
shouldUseEphemeral(channel, requesterChatId) &&
|
|
649
|
+
requesterExternalUserId
|
|
650
|
+
) {
|
|
651
|
+
approvalPayload.ephemeral = true;
|
|
652
|
+
approvalPayload.user = requesterExternalUserId;
|
|
653
|
+
}
|
|
654
|
+
await deliverChannelReply(
|
|
655
|
+
requesterCallbackUrl,
|
|
656
|
+
approvalPayload,
|
|
657
|
+
channelDeliveryContext.bearerToken,
|
|
658
|
+
);
|
|
559
659
|
requesterNotified = true;
|
|
560
660
|
} catch (err) {
|
|
561
661
|
log.error(
|
|
@@ -565,15 +665,23 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
565
665
|
}
|
|
566
666
|
} else {
|
|
567
667
|
try {
|
|
668
|
+
const failurePayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
669
|
+
chatId: requesterTargetChatId,
|
|
670
|
+
text:
|
|
671
|
+
"Your access request was approved, but we were unable to " +
|
|
672
|
+
"deliver the verification code. Please try again later.",
|
|
673
|
+
assistantId,
|
|
674
|
+
};
|
|
675
|
+
if (
|
|
676
|
+
shouldUseEphemeral(channel, requesterChatId) &&
|
|
677
|
+
requesterExternalUserId
|
|
678
|
+
) {
|
|
679
|
+
failurePayload.ephemeral = true;
|
|
680
|
+
failurePayload.user = requesterExternalUserId;
|
|
681
|
+
}
|
|
568
682
|
await deliverChannelReply(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
chatId: requesterChatId,
|
|
572
|
-
text:
|
|
573
|
-
"Your access request was approved, but we were unable to " +
|
|
574
|
-
"deliver the verification code. Please try again later.",
|
|
575
|
-
assistantId,
|
|
576
|
-
},
|
|
683
|
+
requesterCallbackUrl,
|
|
684
|
+
failurePayload,
|
|
577
685
|
channelDeliveryContext.bearerToken,
|
|
578
686
|
);
|
|
579
687
|
} catch (err) {
|
|
@@ -635,8 +743,8 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
635
743
|
}
|
|
636
744
|
|
|
637
745
|
const verificationReplyText = requesterNotified
|
|
638
|
-
? `Access approved for ${requesterLabel}. Give them this verification code:
|
|
639
|
-
: `Access approved for ${requesterLabel}. Give them this verification code:
|
|
746
|
+
? `Access approved for ${requesterLabel}. Give them this verification code: \`${session.secret}\`. The code expires in 10 minutes.`
|
|
747
|
+
: `Access approved for ${requesterLabel}. Give them this verification code: \`${session.secret}\`. The code expires in 10 minutes. I could not notify them automatically, so please tell them to send the code manually.`;
|
|
640
748
|
|
|
641
749
|
return {
|
|
642
750
|
ok: true,
|
|
@@ -686,13 +794,22 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
|
|
|
686
794
|
|
|
687
795
|
if (channelDeliveryContext && requesterChatId) {
|
|
688
796
|
try {
|
|
689
|
-
|
|
690
|
-
channelDeliveryContext.replyCallbackUrl,
|
|
797
|
+
const grantDenialPayload: Parameters<typeof deliverChannelReply>[1] =
|
|
691
798
|
{
|
|
692
799
|
chatId: requesterChatId,
|
|
693
800
|
text: `Your request to use "${request.toolName}" has been denied by the guardian.`,
|
|
694
801
|
assistantId,
|
|
695
|
-
}
|
|
802
|
+
};
|
|
803
|
+
if (
|
|
804
|
+
shouldUseEphemeral(request.sourceChannel ?? "", requesterChatId) &&
|
|
805
|
+
request.requesterExternalUserId
|
|
806
|
+
) {
|
|
807
|
+
grantDenialPayload.ephemeral = true;
|
|
808
|
+
grantDenialPayload.user = request.requesterExternalUserId;
|
|
809
|
+
}
|
|
810
|
+
await deliverChannelReply(
|
|
811
|
+
channelDeliveryContext.replyCallbackUrl,
|
|
812
|
+
grantDenialPayload,
|
|
696
813
|
channelDeliveryContext.bearerToken,
|
|
697
814
|
);
|
|
698
815
|
} catch (err) {
|
|
@@ -775,13 +892,22 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
|
|
|
775
892
|
);
|
|
776
893
|
} else if (channelDeliveryContext && requesterChatId) {
|
|
777
894
|
try {
|
|
778
|
-
|
|
779
|
-
channelDeliveryContext.replyCallbackUrl,
|
|
895
|
+
const grantApprovalPayload: Parameters<typeof deliverChannelReply>[1] =
|
|
780
896
|
{
|
|
781
897
|
chatId: requesterChatId,
|
|
782
898
|
text: `Your request to use "${request.toolName}" has been approved. Please retry your request.`,
|
|
783
899
|
assistantId,
|
|
784
|
-
}
|
|
900
|
+
};
|
|
901
|
+
if (
|
|
902
|
+
shouldUseEphemeral(request.sourceChannel ?? "", requesterChatId) &&
|
|
903
|
+
request.requesterExternalUserId
|
|
904
|
+
) {
|
|
905
|
+
grantApprovalPayload.ephemeral = true;
|
|
906
|
+
grantApprovalPayload.user = request.requesterExternalUserId;
|
|
907
|
+
}
|
|
908
|
+
await deliverChannelReply(
|
|
909
|
+
channelDeliveryContext.replyCallbackUrl,
|
|
910
|
+
grantApprovalPayload,
|
|
785
911
|
channelDeliveryContext.bearerToken,
|
|
786
912
|
);
|
|
787
913
|
} catch (err) {
|